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:
752
architectural-grid_landing/src/components/Sidebar.tsx
Normal file
752
architectural-grid_landing/src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,752 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Plus,
|
||||
Archive,
|
||||
Settings,
|
||||
ChevronRight,
|
||||
BookOpen,
|
||||
Bot,
|
||||
Microscope,
|
||||
Activity,
|
||||
Pin,
|
||||
Moon,
|
||||
Sun,
|
||||
Bell,
|
||||
Lock,
|
||||
Edit3,
|
||||
Trash2,
|
||||
Users,
|
||||
Clock,
|
||||
GripVertical,
|
||||
Wind,
|
||||
Network,
|
||||
Home,
|
||||
Sparkles,
|
||||
LogOut,
|
||||
ChevronDown
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { NavigationView, Carnet, Note, SettingsTab } from '../types';
|
||||
|
||||
interface NoteLinkProps {
|
||||
note: Note;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const NoteLink: React.FC<NoteLinkProps> = ({ note, isActive, onClick }) => (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
onClick={onClick}
|
||||
className={`w-full flex items-center gap-2 pl-12 pr-4 py-2 text-[12px] transition-colors rounded-lg
|
||||
${isActive ? 'bg-white/50 dark:bg-white/10 text-ink font-medium' : 'text-muted-ink hover:text-ink hover:bg-white/30'}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 truncate">
|
||||
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${isActive ? 'bg-ink' : 'bg-transparent border border-muted-ink/30'}`} />
|
||||
<span className="truncate">{note.title}</span>
|
||||
</div>
|
||||
{note.isPinned && <Pin size={10} className="text-amber-500 shrink-0" />}
|
||||
</motion.button>
|
||||
);
|
||||
|
||||
interface SidebarItemProps {
|
||||
carnet: Carnet;
|
||||
isActive: boolean;
|
||||
notes: Note[];
|
||||
activeNoteId: string | null;
|
||||
onCarnetClick: () => void;
|
||||
onNoteClick: (noteId: string) => void;
|
||||
onAddSubCarnet: () => void;
|
||||
onRename: () => void;
|
||||
onDelete: () => void;
|
||||
children?: React.ReactNode;
|
||||
level: number;
|
||||
isExpanded: boolean;
|
||||
toggleExpand: () => void;
|
||||
onMove?: (draggedId: string, targetId?: string) => void;
|
||||
}
|
||||
|
||||
const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||
carnet,
|
||||
isActive,
|
||||
notes,
|
||||
activeNoteId,
|
||||
onCarnetClick,
|
||||
onNoteClick,
|
||||
onAddSubCarnet,
|
||||
onRename,
|
||||
onDelete,
|
||||
children,
|
||||
level,
|
||||
isExpanded,
|
||||
toggleExpand,
|
||||
onMove
|
||||
}) => {
|
||||
const hasChildren = React.Children.count(children) > 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<div
|
||||
className="flex items-center group relative h-10"
|
||||
style={{ paddingLeft: `${level * 12}px` }}
|
||||
>
|
||||
{/* Subtle Drag Handle */}
|
||||
<div className="absolute left-[-2px] opacity-0 group-hover:opacity-40 cursor-grab active:cursor-grabbing text-concrete transition-opacity z-10">
|
||||
<GripVertical size={10} />
|
||||
</div>
|
||||
|
||||
{/* Hierarchy Guide Line */}
|
||||
{level > 0 && (
|
||||
<div className="absolute left-[8px] top-[-10px] bottom-1/2 w-px bg-border/40" />
|
||||
)}
|
||||
{level > 0 && (
|
||||
<div className="absolute left-[8px] top-1/2 w-[8px] h-px bg-border/40" />
|
||||
)}
|
||||
|
||||
<div className="flex-1 flex items-center gap-1">
|
||||
{hasChildren ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleExpand();
|
||||
}}
|
||||
className="p-1 hover:bg-ink/5 dark:hover:bg-white/5 rounded-md transition-colors text-muted-ink"
|
||||
>
|
||||
<motion.div animate={{ rotate: isExpanded ? 90 : 0 }} transition={{ duration: 0.2 }}>
|
||||
<ChevronRight size={14} />
|
||||
</motion.div>
|
||||
</button>
|
||||
) : (
|
||||
<div className="w-6" /> // Spacer for alignment
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
whileHover={{ x: 2 }}
|
||||
className={`flex-1 flex items-center gap-2 px-2 py-1.5 rounded-lg transition-all duration-300 group/item cursor-pointer relative
|
||||
${isActive ? 'bg-white shadow-sm border border-border/40 dark:bg-white/10' : 'hover:bg-white/40 dark:hover:bg-white/5'}`}
|
||||
onClick={onCarnetClick}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
e.currentTarget.classList.add('bg-accent/5', 'ring-1', 'ring-accent/20');
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.currentTarget.classList.remove('bg-accent/5', 'ring-1', 'ring-accent/20');
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.classList.remove('bg-accent/5', 'ring-1', 'ring-accent/20');
|
||||
const draggedId = e.dataTransfer.getData('carnetId');
|
||||
console.log('Dropped carnet:', draggedId, 'on target:', carnet.id);
|
||||
if (draggedId && draggedId !== carnet.id) {
|
||||
onMove?.(draggedId, carnet.id);
|
||||
}
|
||||
}}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
console.log('Starting drag for carnet:', carnet.id);
|
||||
e.dataTransfer.setData('carnetId', carnet.id);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
const ghost = e.currentTarget.cloneNode(true) as HTMLElement;
|
||||
ghost.style.position = 'absolute';
|
||||
ghost.style.top = '-1000px';
|
||||
ghost.style.opacity = '0.5';
|
||||
document.body.appendChild(ghost);
|
||||
e.dataTransfer.setDragImage(ghost, 0, 0);
|
||||
setTimeout(() => document.body.removeChild(ghost), 0);
|
||||
}}
|
||||
>
|
||||
{/* active indicator dot */}
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="active-indicator"
|
||||
className="absolute -left-1 w-1 h-4 bg-accent rounded-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={`w-6 h-6 rounded-md flex items-center justify-center text-[10px] font-bold border transition-all
|
||||
${isActive ? 'bg-accent text-white border-accent' : 'bg-paper dark:bg-white/10 text-concrete border-border dark:border-white/10'}`}>
|
||||
{carnet.initial}
|
||||
</div>
|
||||
<div className="flex-1 text-left flex items-center gap-2 min-w-0">
|
||||
<span className={`text-[12px] font-medium transition-colors truncate ${isActive ? 'text-ink' : 'text-muted-ink group-hover:text-ink'}`}>
|
||||
{carnet.name}
|
||||
</span>
|
||||
{carnet.isPrivate && <Lock size={10} className="text-concrete/60 shrink-0" />}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover/item:opacity-100 transition-opacity shrink-0">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddSubCarnet();
|
||||
}}
|
||||
className="p-1 hover:bg-ink/10 dark:hover:bg-white/10 rounded-md transition-all text-concrete hover:text-ink"
|
||||
title="Add sub-carnet"
|
||||
>
|
||||
<Plus size={10} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRename();
|
||||
}}
|
||||
className="p-1 hover:bg-ink/10 dark:hover:bg-white/10 rounded-md transition-all text-concrete hover:text-ink"
|
||||
title="Rename"
|
||||
>
|
||||
<Edit3 size={10} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
className="p-1 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md transition-all text-concrete hover:text-red-500"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={10} />
|
||||
</button>
|
||||
|
||||
{notes.length > 0 && (
|
||||
<span className="text-[9px] font-bold text-concrete/40 px-1.5 border border-border/40 rounded-full group-hover:text-concrete transition-colors">
|
||||
{notes.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{(isExpanded || (isActive && !hasChildren)) && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="relative" style={{ marginLeft: `${(level + 1) * 12 + 10}px` }}>
|
||||
{/* Vertical line for nested content */}
|
||||
<div className="absolute left-[-6px] top-0 bottom-4 w-px bg-border/30" />
|
||||
|
||||
<div className="space-y-1 py-1">
|
||||
{children}
|
||||
{isActive && !hasChildren && notes.map(note => (
|
||||
<NoteLink
|
||||
key={note.id}
|
||||
note={note}
|
||||
isActive={activeNoteId === note.id}
|
||||
onClick={() => onNoteClick(note.id)}
|
||||
/>
|
||||
))}
|
||||
{isActive && !hasChildren && notes.length === 0 && (
|
||||
<p className="pl-8 py-2 text-[10px] italic text-concrete/40 font-light">
|
||||
No notes found
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SidebarProps {
|
||||
activeView: NavigationView;
|
||||
isSidebarOpen: boolean;
|
||||
setIsSidebarOpen: (val: boolean) => void;
|
||||
isDarkMode: boolean;
|
||||
setIsDarkMode: (val: boolean) => void;
|
||||
setActiveView: (view: NavigationView) => void;
|
||||
setActiveSettingsTab?: (tab: SettingsTab) => void;
|
||||
carnets: Carnet[];
|
||||
notes: Note[];
|
||||
activeCarnetId: string;
|
||||
activeNoteId: string | null;
|
||||
setActiveCarnetId: (id: string) => void;
|
||||
setActiveNoteId: (id: string | null) => void;
|
||||
setShowNewCarnetModal: (show: boolean, parentId?: string, isRenaming?: boolean, carnetId?: string) => void;
|
||||
onDeleteCarnet: (id: string) => void;
|
||||
onMoveCarnet: (draggedId: string, targetId?: string) => void;
|
||||
onGoHome: () => void;
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({
|
||||
activeView,
|
||||
isSidebarOpen,
|
||||
setIsSidebarOpen,
|
||||
isDarkMode,
|
||||
setIsDarkMode,
|
||||
setActiveView,
|
||||
setActiveSettingsTab,
|
||||
carnets,
|
||||
notes,
|
||||
activeCarnetId,
|
||||
activeNoteId,
|
||||
setActiveCarnetId,
|
||||
setActiveNoteId,
|
||||
setShowNewCarnetModal,
|
||||
onDeleteCarnet,
|
||||
onMoveCarnet,
|
||||
onGoHome,
|
||||
onLogout
|
||||
}) => {
|
||||
const [expandedIds, setExpandedIds] = React.useState<Set<string>>(new Set(['4'])); // Default expand Research
|
||||
const [collapsedSections, setCollapsedSections] = React.useState<Set<string>>(new Set());
|
||||
|
||||
const toggleSection = (id: string) => {
|
||||
const newSet = new Set(collapsedSections);
|
||||
if (newSet.has(id)) newSet.delete(id);
|
||||
else newSet.add(id);
|
||||
setCollapsedSections(newSet);
|
||||
};
|
||||
|
||||
const toggleExpand = (id: string) => {
|
||||
const newSet = new Set(expandedIds);
|
||||
if (newSet.has(id)) newSet.delete(id);
|
||||
else newSet.add(id);
|
||||
setExpandedIds(newSet);
|
||||
};
|
||||
|
||||
const renderCarnetTree = (parentId: string | undefined = undefined, level: number = 0) => {
|
||||
return carnets
|
||||
.filter(c => c.parentId === parentId && !c.isDeleted)
|
||||
.map(carnet => (
|
||||
<SidebarItem
|
||||
key={carnet.id}
|
||||
carnet={carnet}
|
||||
isActive={activeCarnetId === carnet.id}
|
||||
notes={notes.filter(n => n.carnetId === carnet.id && !n.isDeleted)}
|
||||
activeNoteId={activeNoteId}
|
||||
level={level}
|
||||
isExpanded={expandedIds.has(carnet.id)}
|
||||
toggleExpand={() => toggleExpand(carnet.id)}
|
||||
onAddSubCarnet={() => {
|
||||
if (!expandedIds.has(carnet.id)) toggleExpand(carnet.id);
|
||||
setShowNewCarnetModal(true, carnet.id);
|
||||
}}
|
||||
onRename={() => {
|
||||
setShowNewCarnetModal(true, undefined, true, carnet.id);
|
||||
}}
|
||||
onDelete={() => {
|
||||
onDeleteCarnet(carnet.id);
|
||||
}}
|
||||
onCarnetClick={() => {
|
||||
setActiveCarnetId(carnet.id);
|
||||
setActiveNoteId(null);
|
||||
// Auto expand when clicking
|
||||
if (!expandedIds.has(carnet.id)) toggleExpand(carnet.id);
|
||||
}}
|
||||
onNoteClick={(id) => {
|
||||
setActiveCarnetId(carnet.id);
|
||||
setActiveNoteId(id);
|
||||
}}
|
||||
onMove={onMoveCarnet}
|
||||
>
|
||||
{renderCarnetTree(carnet.id, level + 1)}
|
||||
</SidebarItem>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{isSidebarOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[60] lg:hidden"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<aside className={`
|
||||
fixed inset-y-0 left-0 lg:relative z-[70] lg:z-20
|
||||
w-80 h-screen bg-white/95 dark:bg-[#0D0D0D]/95 lg:bg-white/30 lg:dark:bg-[#151515]
|
||||
backdrop-blur-md border-r border-border p-6 flex flex-col shrink-0
|
||||
transition-all duration-500 ease-in-out font-sans
|
||||
${isSidebarOpen ? 'translate-x-0 shadow-2xl' : '-translate-x-full lg:translate-x-0'}
|
||||
`}>
|
||||
<div className="mb-8 space-y-4">
|
||||
{/* Header with Logo, Title and Close on Mobile */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 group/logo cursor-pointer" onClick={() => { onGoHome(); setIsSidebarOpen(false); }}>
|
||||
<div className="w-10 h-10 bg-accent flex items-center justify-center rounded-xl shadow-lg shadow-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
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
className="lg:hidden p-2 text-muted-ink hover:text-ink transition-colors"
|
||||
>
|
||||
<ChevronRight size={20} className="rotate-180" />
|
||||
</button>
|
||||
|
||||
<div className="hidden lg:flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveView('settings');
|
||||
setActiveSettingsTab?.('profile');
|
||||
}}
|
||||
className={`w-7 h-7 rounded-full flex items-center justify-center text-[9px] font-bold transition-all border shrink-0
|
||||
${activeView === 'settings'
|
||||
? 'bg-accent text-white border-accent shadow-sm'
|
||||
: 'bg-paper dark:bg-white/5 text-concrete border-border dark:border-white/10 hover:border-accent/40'}`}
|
||||
title="Mon Profil"
|
||||
>
|
||||
S
|
||||
</button>
|
||||
|
||||
<div className="flex items-center bg-white/40 dark:bg-black/20 rounded-lg border border-border/50 p-0.5">
|
||||
<button
|
||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||
className="p-1.5 text-muted-ink hover:text-ink transition-all rounded-md hover:bg-white/50 dark:hover:bg-white/10"
|
||||
title={isDarkMode ? "Passer au clair" : "Passer au sombre"}
|
||||
>
|
||||
{isDarkMode ? <Sun size={12} /> : <Moon size={12} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView('settings')}
|
||||
className={`p-1.5 text-muted-ink hover:text-accent transition-all rounded-md hover:bg-accent/5 ${activeView === 'settings' ? 'text-accent' : ''}`}
|
||||
title="Paramètres"
|
||||
>
|
||||
<Settings size={12} />
|
||||
</button>
|
||||
<button className="p-1.5 text-muted-ink hover:text-rose-500 transition-all rounded-md hover:bg-rose-50 dark:hover:bg-rose-500/5 relative group">
|
||||
<Bell size={12} />
|
||||
<span className="absolute top-1 right-1 w-1.5 h-1.5 bg-rose-500 rounded-full border border-white dark:border-black animate-pulse" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="p-1.5 text-muted-ink hover:text-rose-500 transition-all rounded-md hover:bg-rose-50 dark:hover:bg-rose-500/5"
|
||||
title="Déconnexion"
|
||||
>
|
||||
<LogOut size={12} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Access for Mobile (Moved inside sidebar for better UX) */}
|
||||
<div className="lg:hidden flex items-center justify-between p-3 bg-white/50 dark:bg-white/10 rounded-2xl border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-accent text-white flex items-center justify-center text-[10px] font-bold">S</div>
|
||||
<span className="text-[11px] font-bold text-ink">Sepehr</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||
className="p-2 text-muted-ink hover:text-ink transition-all rounded-lg"
|
||||
>
|
||||
{isDarkMode ? <Sun size={14} /> : <Moon size={14} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="p-2 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-lg transition-all"
|
||||
>
|
||||
<LogOut size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compact View Navigation */}
|
||||
<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')}
|
||||
className={`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="Notes"
|
||||
>
|
||||
<BookOpen size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView('reminders')}
|
||||
className={`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="Rappels"
|
||||
>
|
||||
<Clock size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView('agents')}
|
||||
className={`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="Intelligence Artificielle"
|
||||
>
|
||||
<Bot size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-8 -mx-2 px-2 py-4 custom-scrollbar">
|
||||
{activeView === 'notebooks' ? (
|
||||
<div className="space-y-6">
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-1 rounded-lg transition-colors group/header cursor-pointer"
|
||||
onClick={() => toggleSection('notebooks-tree')}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
e.currentTarget.classList.add('bg-accent/5', 'ring-1', 'ring-accent/20');
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.currentTarget.classList.remove('bg-accent/5', 'ring-1', 'ring-accent/20');
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.classList.remove('bg-accent/5', 'ring-1', 'ring-accent/20');
|
||||
const draggedId = e.dataTransfer.getData('carnetId');
|
||||
console.log('Dropped carnet on root:', draggedId);
|
||||
if (draggedId) {
|
||||
onMoveCarnet(draggedId, undefined);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<motion.div
|
||||
animate={{ rotate: collapsedSections.has('notebooks-tree') ? -90 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<ChevronDown size={10} className="text-concrete" />
|
||||
</motion.div>
|
||||
<p className="text-[10px] font-bold text-concrete tracking-[0.2em] uppercase">
|
||||
Architecture Grid
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowNewCarnetModal(true);
|
||||
}}
|
||||
className="p-1 hover:bg-paper dark:hover:bg-white/5 rounded-md text-concrete hover:text-ink transition-colors opacity-0 group-hover/header:opacity-100"
|
||||
title="New Carnet"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{!collapsedSections.has('notebooks-tree') && (
|
||||
<motion.nav
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="space-y-0.5 overflow-hidden"
|
||||
>
|
||||
{renderCarnetTree()}
|
||||
</motion.nav>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
) : activeView === 'shared' ? (
|
||||
<div className="space-y-6">
|
||||
<div
|
||||
className="flex items-center gap-2 px-4 cursor-pointer group"
|
||||
onClick={() => toggleSection('shared-section')}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ rotate: collapsedSections.has('shared-section') ? -90 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<ChevronDown size={10} className="text-concrete" />
|
||||
</motion.div>
|
||||
<p className="text-[10px] font-bold text-concrete tracking-[0.2em] uppercase">
|
||||
Partagé avec moi
|
||||
</p>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{!collapsedSections.has('shared-section') && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="px-4 py-8 text-center border border-dashed border-border rounded-2xl bg-paper/30 overflow-hidden"
|
||||
>
|
||||
<Users size={24} className="mx-auto text-concrete/40 mb-3" />
|
||||
<p className="text-[11px] text-concrete italic">Aucun document partagé pour le moment.</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
) : activeView === 'reminders' ? (
|
||||
<div className="space-y-6">
|
||||
<div
|
||||
className="flex items-center gap-2 px-4 cursor-pointer group"
|
||||
onClick={() => toggleSection('reminders-section')}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ rotate: collapsedSections.has('reminders-section') ? -90 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<ChevronDown size={10} className="text-concrete" />
|
||||
</motion.div>
|
||||
<p className="text-[10px] font-bold text-concrete tracking-[0.2em] uppercase">
|
||||
Rappels programmés
|
||||
</p>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{!collapsedSections.has('reminders-section') && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="px-4 py-8 text-center border border-dashed border-border rounded-2xl bg-paper/30 overflow-hidden"
|
||||
>
|
||||
<Clock size={24} className="mx-auto text-concrete/40 mb-3" />
|
||||
<p className="text-[11px] text-concrete italic">Aucun rappel actif.</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
) : activeView === 'agents' ? (
|
||||
<div className="space-y-6">
|
||||
<div
|
||||
className="flex items-center gap-2 px-4 cursor-pointer group"
|
||||
onClick={() => toggleSection('agents-section')}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ rotate: collapsedSections.has('agents-section') ? -90 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<ChevronDown size={10} className="text-concrete" />
|
||||
</motion.div>
|
||||
<p className="text-[10px] font-bold text-muted-ink tracking-[0.2em] uppercase">
|
||||
Intelligence OS
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{!collapsedSections.has('agents-section') && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="space-y-8 overflow-hidden"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{[
|
||||
{ id: 'a1', name: 'Mes Agents', icon: <Bot size={16} /> },
|
||||
{ id: 'a2', name: 'Le Lab AI', icon: <Microscope size={16} /> },
|
||||
{ id: 'a3', name: 'Activités', icon: <Activity size={16} /> },
|
||||
].map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 group
|
||||
${item.id === 'a1' ? 'active-nav-item' : 'text-muted-ink hover:bg-white/40 dark:hover:bg-white/5 hover:text-ink'}`}
|
||||
>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center border transition-colors
|
||||
${item.id === 'a1' ? 'bg-ink text-paper border-ink' : 'bg-white/60 dark:bg-white/10 border-border dark:border-white/10 group-hover:border-ink/20'}`}>
|
||||
{item.icon}
|
||||
</div>
|
||||
<span className="text-[13px] font-medium">{item.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] font-bold text-concrete tracking-[0.2em] uppercase px-4 mb-2">
|
||||
Capabilities
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setActiveView('brainstorm')}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 text-[13px] transition-all font-medium group rounded-xl text-muted-ink hover:text-ochre hover:bg-ochre/5"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center border bg-white/60 dark:bg-white/10 border-border dark:border-white/10 group-hover:border-ochre/20">
|
||||
<Wind size={16} />
|
||||
</div>
|
||||
<span className="flex-1 text-left">Brainstorm Wave</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveView('insights')}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 text-[13px] transition-all font-medium group rounded-xl text-muted-ink hover:text-indigo-500 hover:bg-indigo-500/5"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center border bg-white/60 dark:bg-white/10 border-border dark:border-white/10 group-hover:border-indigo-500/20">
|
||||
<Network size={16} />
|
||||
</div>
|
||||
<span className="flex-1 text-left">Semantic Network</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveView('temporal')}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 text-[13px] transition-all font-medium group rounded-xl text-muted-ink hover:text-rose-500 hover:bg-rose-500/5"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center border bg-white/60 dark:bg-white/10 border-border dark:border-white/10 group-hover:border-rose-500/20">
|
||||
<Clock size={16} />
|
||||
</div>
|
||||
<span className="flex-1 text-left">Temporal Forecast</span>
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border/40 mt-auto pb-4 space-y-4">
|
||||
{/* AI Quota Tracker */}
|
||||
<div className="px-4">
|
||||
<div className="p-4 bg-slate-50 dark:bg-white/5 border border-border rounded-2xl space-y-3 group hover:shadow-lg transition-all duration-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles size={12} className="text-accent fill-accent/20" />
|
||||
<span className="text-[11px] font-bold text-ink/70">Pack découverte IA</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-medium text-concrete">49 restants</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full bg-slate-200 dark:bg-white/10 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: '49%' }}
|
||||
className="h-full bg-accent rounded-full shadow-[0_0_8px_rgba(var(--color-accent),0.5)]"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveView('settings');
|
||||
setActiveSettingsTab?.('billing');
|
||||
}}
|
||||
className="w-full py-2 bg-accent/10 hover:bg-accent text-accent hover:text-white text-[9px] font-bold uppercase tracking-widest rounded-xl transition-all duration-300 border border-accent/20"
|
||||
>
|
||||
Passer à Pro
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-2 space-y-0.5">
|
||||
<button
|
||||
onClick={() => setActiveView('shared')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 text-[12px] transition-all font-medium group rounded-xl ${activeView === 'shared' ? 'bg-accent/5 text-accent' : 'text-muted-ink hover:text-ink hover:bg-black/5'}`}
|
||||
>
|
||||
<Users size={14} className={activeView === 'shared' ? 'text-accent' : 'text-muted-ink group-hover:text-ink'} />
|
||||
<span className="flex-1 text-left">Partagé</span>
|
||||
</button>
|
||||
|
||||
<button 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 className="flex-1 text-left">Archives</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveView('trash')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 text-[12px] transition-all font-medium group rounded-xl ${activeView === 'trash' ? 'bg-rose-50 text-rose-500' : 'text-muted-ink hover:text-rose-500 hover:bg-rose-50/50'}`}
|
||||
>
|
||||
<Trash2 size={14} className={activeView === 'trash' ? 'text-rose-500' : 'text-muted-ink group-hover:text-rose-500'} />
|
||||
<span className="flex-1 text-left">Corbeille</span>
|
||||
{notes.some(n => n.isDeleted) && (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-rose-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user