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>
119 lines
3.8 KiB
TypeScript
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>
|
|
)
|
|
}
|