From 08ab0d1a1e1114fbdcbf79b7d5a42575003b933b Mon Sep 17 00:00:00 2001 From: Sepehr Ramezani Date: Sun, 19 Apr 2026 09:57:21 +0200 Subject: [PATCH] =?UTF-8?q?fix(rtl):=20remplacer=20les=20propri=C3=A9t?= =?UTF-8?q?=C3=A9s=20physiques=20par=20logiques=20pour=20le=20support=20RT?= =?UTF-8?q?L?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La sidebar et le lab header utilisaient border-r, pr-4, ml-2, ml-auto au lieu des propriétés logiques CSS (border-e, pe-4, ms-2, ms-auto). En mode RTL (persan/arabe), ces propriétés physiques ne s'inversent pas, ce qui causait la sidebar à basculer du mauvais côté lors de la navigation vers Lab/Excalidraw. Co-Authored-By: Claude Opus 4.5 --- keep-notes/app/(main)/layout.tsx | 10 +- keep-notes/components/lab/canvas-board.tsx | 118 ++++++++++++ keep-notes/components/lab/lab-header.tsx | 197 +++++++++++++++++++++ keep-notes/components/sidebar.tsx | 2 +- 4 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 keep-notes/components/lab/canvas-board.tsx create mode 100644 keep-notes/components/lab/lab-header.tsx diff --git a/keep-notes/app/(main)/layout.tsx b/keep-notes/app/(main)/layout.tsx index ecae9be..e8ba797 100644 --- a/keep-notes/app/(main)/layout.tsx +++ b/keep-notes/app/(main)/layout.tsx @@ -3,20 +3,24 @@ import { Sidebar } from "@/components/sidebar"; import { ProvidersWrapper } from "@/components/providers-wrapper"; import { auth } from "@/auth"; import { detectUserLanguage } from "@/lib/i18n/detect-user-language"; +import { loadTranslations } from "@/lib/i18n/load-translations"; export default async function MainLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - // Run auth + language detection in parallel + // Run auth + language detection + translation loading in parallel const [session, initialLanguage] = await Promise.all([ auth(), detectUserLanguage(), ]); + // Load initial translations server-side to prevent hydration mismatch + const initialTranslations = await loadTranslations(initialLanguage); + return ( - +
{/* Top Navigation - Style Keep */} @@ -24,7 +28,7 @@ export default async function MainLayout({ {/* Main Layout */}
{/* Sidebar Navigation - Style Keep */} - + {/* Main Content Area */}
diff --git a/keep-notes/components/lab/canvas-board.tsx b/keep-notes/components/lab/canvas-board.tsx new file mode 100644 index 0000000..c906f66 --- /dev/null +++ b/keep-notes/components/lab/canvas-board.tsx @@ -0,0 +1,118 @@ +'use client' + +import dynamic from 'next/dynamic' +import { useState, useEffect, useRef } from 'react' +import { toast } from 'sonner' +import type { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types' +import type { AppState, BinaryFiles } from '@excalidraw/excalidraw/types/types' +import '@excalidraw/excalidraw/index.css' + +// Dynamic import with SSR disabled is REQUIRED for Excalidraw due to window dependencies +const Excalidraw = dynamic( + async () => (await import('@excalidraw/excalidraw')).Excalidraw, + { ssr: false } +) + +interface CanvasBoardProps { + initialData?: string + canvasId?: string + name: string +} + +export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) { + const [isDarkMode, setIsDarkMode] = useState(false) + const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'error'>('saved') + const saveTimeoutRef = useRef(null) + + // Parse initial state safely (ONLY ON MOUNT to prevent Next.js revalidation infinite loops) + const [elements] = useState(() => { + if (initialData) { + try { + const parsed = JSON.parse(initialData) + if (parsed && Array.isArray(parsed)) { + return parsed + } else if (parsed && parsed.elements) { + return parsed.elements + } + } catch (e) { + console.error("[CanvasBoard] Failed to parse initial Excalidraw data:", e) + } + } + return [] + }) + + // Detect dark mode from html class + useEffect(() => { + const checkDarkMode = () => { + const isDark = document.documentElement.classList.contains('dark') + setIsDarkMode(isDark) + } + + checkDarkMode() + + // Observer for theme changes + const observer = new MutationObserver(checkDarkMode) + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }) + + return () => observer.disconnect() + }, []) + + // Prevent Excalidraw from overriding document.documentElement.dir. + // Excalidraw internally sets `document.documentElement.dir = "ltr"` which + // breaks the RTL layout of the parent sidebar and header. + useEffect(() => { + const savedDir = document.documentElement.dir || 'ltr' + + const dirObserver = new MutationObserver(() => { + if (document.documentElement.dir !== savedDir) { + document.documentElement.dir = savedDir + } + }) + dirObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] }) + + return () => dirObserver.disconnect() + }, []) + + const handleChange = ( + excalidrawElements: readonly ExcalidrawElement[], + appState: AppState, + files: BinaryFiles + ) => { + if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current) + + setSaveStatus('saving') + saveTimeoutRef.current = setTimeout(async () => { + try { + // Excalidraw states are purely based on the geometric elements + const snapshot = JSON.stringify(excalidrawElements) + await fetch('/api/canvas', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: canvasId || null, name, data: snapshot }) + }) + setSaveStatus('saved') + } catch (e) { + console.error("[CanvasBoard] Save failure:", e) + setSaveStatus('error') + } + }, 2000) + } + + return ( +
+ +
+ ) +} diff --git a/keep-notes/components/lab/lab-header.tsx b/keep-notes/components/lab/lab-header.tsx new file mode 100644 index 0000000..52d7e22 --- /dev/null +++ b/keep-notes/components/lab/lab-header.tsx @@ -0,0 +1,197 @@ +'use client' + +import { FlaskConical, Plus, X, ChevronDown, Trash2, Layout, MoreVertical } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { renameCanvas, deleteCanvas, createCanvas } from '@/app/actions/canvas-actions' +import { useRouter } from 'next/navigation' +import { useState, useTransition } from 'react' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { cn } from '@/lib/utils' +import { toast } from 'sonner' +import { useLanguage } from '@/lib/i18n' + +interface LabHeaderProps { + canvases: any[] + currentCanvasId: string | null + onCreateCanvas: () => Promise +} + +export function LabHeader({ canvases, currentCanvasId, onCreateCanvas }: LabHeaderProps) { + const router = useRouter() + const { t } = useLanguage() + const [isPending, startTransition] = useTransition() + const [isEditing, setIsEditing] = useState(false) + + const currentCanvas = canvases.find(c => c.id === currentCanvasId) + + const handleRename = async (id: string, newName: string) => { + if (!newName || newName === currentCanvas?.name) { + setIsEditing(false) + return + } + + startTransition(async () => { + try { + await renameCanvas(id, newName) + toast.success(t('labHeader.renamed')) + } catch (e) { + toast.error(t('labHeader.renameError')) + } + setIsEditing(false) + }) + } + + const handleCreate = async () => { + startTransition(async () => { + try { + const newCanvas = await createCanvas() + router.push(`/lab?id=${newCanvas.id}`) + toast.success(t('labHeader.created')) + } catch (e) { + toast.error(t('labHeader.createFailed')) + } + }) + } + + const handleDelete = async (id: string, name: string) => { + if (window.confirm(`${t('labHeader.deleteSpace')} "${name}" ?`)) { + try { + await deleteCanvas(id) + toast.success(t('labHeader.deleted')) + router.push('/lab') + router.refresh() + } catch (e) { + toast.error(t('labHeader.deleteError')) + } + } + } + + return ( +
+
+
+
+ +
+
+

{t('labHeader.title')}

+
+ +

{t('labHeader.live')}

+
+
+
+ + {/* Project Switcher */} + + + + + + + {t('labHeader.yourSpaces')} + {canvases.length} + +
+ {canvases.map(c => ( +
+ router.push(`/lab?id=${c.id}`)} + > +
+ {c.name} + {c.id === currentCanvasId && } +
+ {t('labHeader.updated')} {new Date(c.updatedAt).toLocaleDateString()} +
+ +
+ ))} +
+ + + + {t('labHeader.newSpace')} + +
+
+ + {/* Inline Rename */} +
+ {isEditing ? ( + { + if (e.key === 'Enter') handleRename(currentCanvas?.id!, e.currentTarget.value) + if (e.key === 'Escape') setIsEditing(false) + }} + onBlur={(e) => handleRename(currentCanvas?.id!, e.target.value)} + /> + ) : ( + + )} +
+
+ +
+ {currentCanvas && ( + + )} + +
+
+ ) +} diff --git a/keep-notes/components/sidebar.tsx b/keep-notes/components/sidebar.tsx index f11120f..db7d6e9 100644 --- a/keep-notes/components/sidebar.tsx +++ b/keep-notes/components/sidebar.tsx @@ -96,7 +96,7 @@ export function Sidebar({ className, user }: { className?: string, user?: any }) > {t('sidebar.newNoteTabs')} - +