fix(insights): sidebar user-controlled + cluster chips organisées
Sidebar (UX: User Freedom): - Visible par défaut sur /insights (pas caché par le système) - Toggle via bouton hamburger: toggle-insights-sidebar event - Préférence persistée en localStorage (insights-sidebar-collapsed) - User décide: voir ou cacher le sidebar Cluster chips: - Triées par taille (plus grand cluster en premier) - Compteur de notes par cluster (badge arrondi) - max-w-[120px] sur le nom pour éviter overflow - padding/gap plus compacts (px-2.5 py-1 gap-1.5)
This commit is contained in:
@@ -298,8 +298,8 @@ export default function InsightsPage() {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="p-2 -ms-1 text-foreground hover:bg-foreground/5 rounded-lg transition-colors shrink-0 cursor-pointer focus-visible:ring-2 focus-visible:ring-ochre/50 focus-visible:outline-none"
|
className="p-2 -ms-1 text-foreground hover:bg-foreground/5 rounded-lg transition-colors shrink-0 cursor-pointer focus-visible:ring-2 focus-visible:ring-ochre/50 focus-visible:outline-none"
|
||||||
onClick={() => window.dispatchEvent(new CustomEvent('open-mobile-sidebar'))}
|
onClick={() => window.dispatchEvent(new CustomEvent('toggle-insights-sidebar'))}
|
||||||
aria-label={t('sidebar.openNavigation') || 'Open navigation'}
|
aria-label="Toggle sidebar"
|
||||||
>
|
>
|
||||||
<Menu size={22} />
|
<Menu size={22} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -362,22 +362,27 @@ export function NetworkGraph({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="w-full h-full bg-paper dark:bg-[#121212] rounded-3xl overflow-hidden border border-border/40 relative">
|
<div ref={containerRef} className="w-full h-full bg-paper dark:bg-[#121212] rounded-3xl overflow-hidden border border-border/40 relative">
|
||||||
{/* Pastilles de cluster — cliquables pour activer le focus */}
|
{/* Pastilles de cluster — triées par taille, avec compteurs */}
|
||||||
<div className="absolute top-6 left-6 z-10 flex flex-wrap gap-2 max-w-[90%]">
|
<div className="absolute top-6 left-6 z-10 flex flex-wrap gap-1.5 max-w-[85%]">
|
||||||
{clusters.map(c => {
|
{[...clusters]
|
||||||
|
.sort((a, b) => b.noteIds.length - a.noteIds.length)
|
||||||
|
.map(c => {
|
||||||
const isSelected = String(c.id) === selectedClusterId
|
const isSelected = String(c.id) === selectedClusterId
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={c.id}
|
key={c.id}
|
||||||
onClick={() => onClusterSelect?.(isSelected ? null : String(c.id))}
|
onClick={() => onClusterSelect?.(isSelected ? null : String(c.id))}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full border shadow-sm transition-all text-[9px] font-bold uppercase tracking-wider ${
|
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full border transition-all text-[9px] font-bold uppercase tracking-wider cursor-pointer focus-visible:ring-2 focus-visible:ring-ochre/50 focus-visible:outline-none ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-ink text-white dark:bg-white dark:text-black border-ink dark:border-white scale-105 shadow-md'
|
? 'bg-ink text-white dark:bg-white dark:text-black border-ink dark:border-white scale-105 shadow-md'
|
||||||
: 'bg-white/90 dark:bg-black/80 text-concrete hover:text-ink hover:border-concrete/40 border-border'
|
: 'bg-white/90 dark:bg-black/80 text-concrete hover:text-ink hover:border-concrete/40 border-border shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: c.color }} />
|
<div className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: c.color }} />
|
||||||
<span>{c.name ?? String(c.id)}</span>
|
<span className="truncate max-w-[120px]">{c.name ?? String(c.id)}</span>
|
||||||
|
<span className={`text-[8px] px-1 rounded-full shrink-0 ${isSelected ? 'bg-white/20' : 'bg-black/5 dark:bg-white/10'}`}>
|
||||||
|
{c.noteIds.length}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -703,6 +703,27 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
|||||||
const isRemindersRoute = pathname === '/home' && searchParams.get('reminders') === '1'
|
const isRemindersRoute = pathname === '/home' && searchParams.get('reminders') === '1'
|
||||||
const isSharedRoute = pathname === '/home' && searchParams.get('shared') === '1'
|
const isSharedRoute = pathname === '/home' && searchParams.get('shared') === '1'
|
||||||
const isImmersiveRoute = pathname === '/insights'
|
const isImmersiveRoute = pathname === '/insights'
|
||||||
|
|
||||||
|
// Sur /insights : sidebar visible par défaut, user peut le cacher via toggle
|
||||||
|
const [userCollapsed, setUserCollapsed] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isImmersiveRoute) {
|
||||||
|
const stored = localStorage.getItem('insights-sidebar-collapsed')
|
||||||
|
setUserCollapsed(stored === 'true')
|
||||||
|
}
|
||||||
|
}, [isImmersiveRoute])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onToggle = () => {
|
||||||
|
setUserCollapsed(prev => {
|
||||||
|
const next = !prev
|
||||||
|
if (isImmersiveRoute) localStorage.setItem('insights-sidebar-collapsed', String(next))
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
window.addEventListener('toggle-insights-sidebar', onToggle)
|
||||||
|
return () => window.removeEventListener('toggle-insights-sidebar', onToggle)
|
||||||
|
}, [isImmersiveRoute])
|
||||||
const isNotebooksRoute =
|
const isNotebooksRoute =
|
||||||
(pathname === '/home' || pathname.startsWith('/notes')) &&
|
(pathname === '/home' || pathname.startsWith('/notes')) &&
|
||||||
!pathname.startsWith('/settings') &&
|
!pathname.startsWith('/settings') &&
|
||||||
@@ -1158,15 +1179,13 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
|||||||
|
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
// Sur /insights : sidebar en overlay (toutes tailles d'écran)
|
isImmersiveRoute && userCollapsed
|
||||||
// Ailleurs : overlay sur mobile, relative sur desktop
|
|
||||||
isImmersiveRoute
|
|
||||||
? 'fixed inset-y-0 start-0 z-[70]'
|
? 'fixed inset-y-0 start-0 z-[70]'
|
||||||
: 'fixed inset-y-0 start-0 z-[70] md:relative md:z-auto',
|
: 'fixed inset-y-0 start-0 z-[70] md:relative md:z-auto',
|
||||||
'h-full min-h-0 w-80 xl:w-[22rem] 2xl:w-[26rem] shrink-0 flex flex-row overflow-hidden',
|
'h-full min-h-0 w-80 xl:w-[22rem] 2xl:w-[26rem] shrink-0 flex flex-row overflow-hidden',
|
||||||
'transition-transform duration-300 ease-in-out',
|
'transition-transform duration-300 ease-in-out',
|
||||||
isImmersiveRoute
|
isImmersiveRoute && userCollapsed
|
||||||
? (isMobileOpen ? 'translate-x-0 shadow-2xl' : '-translate-x-full')
|
? '-translate-x-full'
|
||||||
: (isMobileOpen ? 'translate-x-0 shadow-2xl' : '-translate-x-full md:translate-x-0'),
|
: (isMobileOpen ? 'translate-x-0 shadow-2xl' : '-translate-x-full md:translate-x-0'),
|
||||||
'border-e border-border/40 bg-white/95 md:bg-white/30 backdrop-blur-md sidebar-shadow dark:border-white/6 dark:bg-[#151515] dark:backdrop-blur-none',
|
'border-e border-border/40 bg-white/95 md:bg-white/30 backdrop-blur-md sidebar-shadow dark:border-white/6 dark:bg-[#151515] dark:backdrop-blur-none',
|
||||||
className
|
className
|
||||||
|
|||||||
Reference in New Issue
Block a user