diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c7239a4..bcefc73 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,6 +23,7 @@ import { Splitter } from "./Splitter"; import { SettingsDialog, DEFAULT_TITLE } from "./SettingsDialog"; import { pendingVersion, useUpdateStatus } from "./update"; import { storeThumb } from "./thumbs"; +import { fitScene } from "./fit"; type Theme = "light" | "dark"; type SaveState = "saved" | "dirty" | "saving" | "error"; @@ -105,6 +106,7 @@ export default function App() { const libTimer = useRef(undefined); const libJson = useRef(""); // dernier état connu côté serveur const sceneSeq = useRef(0); + const fitPending = useRef(false); // cadrage à faire au premier rendu du schéma ouvert const exTheme = useRef(null); // dernier thème signalé par Excalidraw const openRef = useRef<(path: string, opts?: { skipFlush?: boolean }) => Promise>(async () => {}); @@ -169,6 +171,10 @@ export default function App() { if (appState.theme !== theme) setTheme(appState.theme as Theme); } if (!pathRef.current || appState.isLoading) return; + if (fitPending.current && excalidraw.current) { + fitPending.current = false; + fitScene(excalidraw.current); + } const version = sceneVersion(elements, appState, files); // Premier onChange après chargement : c'est la version de référence. if (savedVersion.current === "") { @@ -195,6 +201,7 @@ export default function App() { etagRef.current = etag; savedVersion.current = ""; // fixée au premier onChange (voir onChange) pathRef.current = path; + fitPending.current = true; setScene({ id: ++sceneSeq.current, path, @@ -202,7 +209,6 @@ export default function App() { elements: data.elements ?? [], appState: { ...(data.appState ?? {}), theme }, files: data.files ?? {}, - scrollToContent: true, }, }); setSaveState("saved"); diff --git a/frontend/src/ShareView.tsx b/frontend/src/ShareView.tsx index 36b1a2e..8f2be84 100644 --- a/frontend/src/ShareView.tsx +++ b/frontend/src/ShareView.tsx @@ -1,8 +1,9 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { Excalidraw } from "@excalidraw/excalidraw"; -import type { ExcalidrawInitialDataState } from "@excalidraw/excalidraw/types"; +import type { AppState, ExcalidrawImperativeAPI, ExcalidrawInitialDataState } from "@excalidraw/excalidraw/types"; import { api, type PublicScene } from "./api"; import { DEFAULT_TITLE } from "./SettingsDialog"; +import { fitScene } from "./fit"; import { DownloadIcon, EyeIcon, MoonIcon, SunIcon } from "./icons"; type Theme = "light" | "dark"; @@ -22,6 +23,21 @@ export function ShareView({ token }: { token: string }) { const [data, setData] = useState(null); const [error, setError] = useState(null); const [theme, setTheme] = useState(initialTheme); + const excalidraw = useRef(null); + const fitted = useRef(false); + + const onApi = useCallback((ex: ExcalidrawImperativeAPI | null) => { + excalidraw.current = ex; + }, []); + // excalidrawAPI jusqu'à la 0.18, onExcalidrawAPI sur le canal next. + const apiProps = { excalidrawAPI: onApi, onExcalidrawAPI: onApi } as object; + + // Premier rendu : cadrage sur l'ensemble du schéma. + const onChange = useCallback((_elements: unknown, appState: AppState) => { + if (appState.isLoading || fitted.current || !excalidraw.current) return; + fitted.current = true; + fitScene(excalidraw.current); + }, []); useEffect(() => { api @@ -51,7 +67,6 @@ export function ShareView({ token }: { token: string }) { elements: (data.scene.elements ?? []) as ExcalidrawInitialDataState["elements"], appState: { ...(data.scene.appState ?? {}), theme } as ExcalidrawInitialDataState["appState"], files: (data.scene.files ?? {}) as ExcalidrawInitialDataState["files"], - scrollToContent: true, }; return ( @@ -96,6 +111,8 @@ export function ShareView({ token }: { token: string }) { window.matchMedia("(max-width: 768px)").matches; + +/** + * Cadre tout le schéma dans la zone visible, sans zoomer au-delà de 100 %. + * + * Remplace `initialData.scrollToContent` : quand le contenu déborde de la + * fenêtre, Excalidraw centre alors un seul élément (le plus proche de l'origine) + * au lieu de l'ensemble, et le schéma s'affiche à partir du milieu de l'écran. + */ +export function fitScene(ex: ExcalidrawImperativeAPI) { + const elements = ex.getSceneElements(); + if (elements.length === 0) return; + ex.scrollToContent(elements, { + fitToContent: true, + viewportZoomFactor: 0.9, + animate: false, + // Barres d'outils d'Excalidraw posées sur le canevas (en haut, et en bas sur mobile). + canvasOffsets: { top: 64, bottom: isMobile() ? 64 : 0 }, + }); +}