feat: sidebar redimensionnable — drag handle entre sidebar et contenu
- sidebar-resize.tsx: hook useSidebarResize (localStorage persistence, 280-560px) - sidebar-resize-client.tsx: wrapper layout qui applique la largeur - layout.tsx: remplace div statique par SidebarResizeClient - Resize handle: barre 1px→3px au hover, brand-accent pendant drag - pointer events (pas mouse) pour support tactile - cursor: col-resize + userSelect:none pendant le drag L'utilisateur peut maintenant réduire ou élargir la zone de carnet en glissant la barre entre le sidebar et le contenu principal.
This commit is contained in:
@@ -7,6 +7,7 @@ import { detectUserLanguage, parseAcceptLanguage } from "@/lib/i18n/detect-user-
|
|||||||
import { loadTranslations } from "@/lib/i18n/load-translations";
|
import { loadTranslations } from "@/lib/i18n/load-translations";
|
||||||
import { getAISettings } from "@/app/actions/ai-settings";
|
import { getAISettings } from "@/app/actions/ai-settings";
|
||||||
import { AIChatLayoutBridge } from "@/components/ai-chat-layout-bridge";
|
import { AIChatLayoutBridge } from "@/components/ai-chat-layout-bridge";
|
||||||
|
import { SidebarResizeClient } from "@/components/sidebar-resize-client";
|
||||||
|
|
||||||
|
|
||||||
export default async function MainLayout({
|
export default async function MainLayout({
|
||||||
@@ -36,8 +37,8 @@ export default async function MainLayout({
|
|||||||
initialAiProcessingConsent={aiSettings?.aiProcessingConsent === true}
|
initialAiProcessingConsent={aiSettings?.aiProcessingConsent === true}
|
||||||
>
|
>
|
||||||
{/* No top-bar header — sidebar-only navigation (architectural-grid design) */}
|
{/* No top-bar header — sidebar-only navigation (architectural-grid design) */}
|
||||||
<div className="flex h-screen overflow-hidden bg-memento-desk dark:bg-background">
|
<SidebarResizeClient>
|
||||||
<Suspense fallback={<div className="hidden w-80 md:w-[26rem] 2xl:w-[30rem] shrink-0 md:block h-full self-stretch" />}>
|
<Suspense fallback={<div className="hidden w-80 shrink-0 md:block h-full self-stretch" />}>
|
||||||
<Sidebar user={session?.user} />
|
<Sidebar user={session?.user} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
@@ -47,9 +48,9 @@ export default async function MainLayout({
|
|||||||
</a>
|
</a>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
</SidebarResizeClient>
|
||||||
|
|
||||||
{showAIAssistant && <AIChatLayoutBridge />}
|
{showAIAssistant && <AIChatLayoutBridge />}
|
||||||
</div>
|
|
||||||
</ProvidersWrapper>
|
</ProvidersWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
19
memento-note/components/sidebar-resize-client.tsx
Normal file
19
memento-note/components/sidebar-resize-client.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { type ReactNode } from 'react'
|
||||||
|
import { useSidebarResize, SidebarResizeHandle } from '@/components/sidebar-resize'
|
||||||
|
|
||||||
|
export function SidebarResizeClient({ children }: { children: ReactNode }) {
|
||||||
|
const { width, isDragging, onPointerDown } = useSidebarResize()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen overflow-hidden bg-memento-desk dark:bg-background">
|
||||||
|
<div className="contents" style={{ ['--sidebar-width' as string]: `${width}px` }}>
|
||||||
|
<div className="flex h-full" style={{ width: `${width}px` }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SidebarResizeHandle onPointerDown={onPointerDown} isDragging={isDragging} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
81
memento-note/components/sidebar-resize.tsx
Normal file
81
memento-note/components/sidebar-resize.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'memento-sidebar-width'
|
||||||
|
const MIN = 280
|
||||||
|
const MAX = 560
|
||||||
|
|
||||||
|
export function useSidebarResize() {
|
||||||
|
const [width, setWidth] = useState(320)
|
||||||
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
const startXRef = useRef(0)
|
||||||
|
const startWidthRef = useRef(320)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (stored) {
|
||||||
|
const parsed = parseInt(stored, 10)
|
||||||
|
if (!isNaN(parsed) && parsed >= MIN && parsed <= MAX) {
|
||||||
|
setWidth(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onPointerDown = useCallback((e: React.PointerEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsDragging(true)
|
||||||
|
startXRef.current = e.clientX
|
||||||
|
startWidthRef.current = width
|
||||||
|
document.body.style.cursor = 'col-resize'
|
||||||
|
document.body.style.userSelect = 'none'
|
||||||
|
}, [width])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDragging) return
|
||||||
|
|
||||||
|
const onPointerMove = (e: PointerEvent) => {
|
||||||
|
const delta = e.clientX - startXRef.current
|
||||||
|
const newWidth = Math.max(MIN, Math.min(MAX, startWidthRef.current + delta))
|
||||||
|
setWidth(newWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerUp = () => {
|
||||||
|
setIsDragging(false)
|
||||||
|
document.body.style.cursor = ''
|
||||||
|
document.body.style.userSelect = ''
|
||||||
|
setWidth(prev => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, String(prev))
|
||||||
|
return prev
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('pointermove', onPointerMove)
|
||||||
|
document.addEventListener('pointerup', onPointerUp)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('pointermove', onPointerMove)
|
||||||
|
document.removeEventListener('pointerup', onPointerUp)
|
||||||
|
}
|
||||||
|
}, [isDragging])
|
||||||
|
|
||||||
|
return { width, isDragging, onPointerDown }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SidebarResizeHandle({ onPointerDown, isDragging }: { onPointerDown: (e: React.PointerEvent) => void; isDragging: boolean }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
className={`hidden md:flex items-center justify-center w-1 shrink-0 cursor-col-resize transition-colors z-50 ${
|
||||||
|
isDragging
|
||||||
|
? 'bg-brand-accent w-[3px]'
|
||||||
|
: 'bg-border/40 hover:bg-brand-accent/50 hover:w-[3px]'
|
||||||
|
}`}
|
||||||
|
style={{ touchAction: 'none' }}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div className={`w-[2px] h-12 rounded-full transition-colors ${isDragging ? 'bg-brand-accent' : 'bg-border/60'}`} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MIN as SIDEBAR_MIN_WIDTH, MAX as SIDEBAR_MAX_WIDTH }
|
||||||
Reference in New Issue
Block a user