Files
Keep/keep-notes/components/lab/canvas-board.tsx
Sepehr Ramezani 08ab0d1a1e fix(rtl): remplacer les propriétés physiques par logiques pour le support RTL
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 <noreply@anthropic.com>
2026-04-19 10:01:10 +02:00

119 lines
3.8 KiB
TypeScript

'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<NodeJS.Timeout | null>(null)
// Parse initial state safely (ONLY ON MOUNT to prevent Next.js revalidation infinite loops)
const [elements] = useState<readonly ExcalidrawElement[]>(() => {
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 (
<div className="absolute inset-0 h-full w-full bg-slate-50 dark:bg-[#121212]" dir="ltr">
<Excalidraw
initialData={{ elements }}
theme={isDarkMode ? "dark" : "light"}
onChange={handleChange}
libraryReturnUrl={typeof window !== 'undefined' ? window.location.origin + window.location.pathname + window.location.search : undefined}
UIOptions={{
canvasActions: {
loadScene: false,
saveToActiveFile: false,
clearCanvas: true
}
}}
/>
</div>
)
}