From fc4ef29230f09a549df364729ca101e9c2168c39 Mon Sep 17 00:00:00 2001 From: MeghdadFadaee Date: Sun, 14 Jun 2026 18:42:14 +0330 Subject: [PATCH] Implement responsive mobile support. --- index.html | 2 +- src/App.tsx | 107 +++++++++++++++++++++++++++++++++++++++---- src/styles.css | 44 ++++++++++++++++-- src/viewport.test.ts | 6 +++ 4 files changed, 146 insertions(+), 13 deletions(-) diff --git a/index.html b/index.html index 6d6c69d..f92721c 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + OpenXD diff --git a/src/App.tsx b/src/App.tsx index 1a785d3..ba58d2b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -53,9 +53,23 @@ export default function App() { const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [warningsOpen, setWarningsOpen] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(false); const dragRef = useRef<{ x: number; y: number; originX: number; originY: number } | null>(null); + const panRef = useRef(pan); + const zoomRef = useRef(zoom); + const touchRef = useRef<{ + mode: "pan" | "pinch"; + x: number; + y: number; + distance: number; + pan: { x: number; y: number }; + zoom: number; + } | null>(null); const canvasRef = useRef(null); + useEffect(() => { panRef.current = pan; }, [pan]); + useEffect(() => { zoomRef.current = zoom; }, [zoom]); + const fit = useCallback(() => { if (!document) return; const canvas = canvasRef.current?.getBoundingClientRect(); @@ -68,6 +82,71 @@ export default function App() { }, [bounds, document]); useEffect(() => { fit(); }, [fit]); + useEffect(() => { + const refit = () => fit(); + window.addEventListener("resize", refit); + return () => window.removeEventListener("resize", refit); + }, [fit]); + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas || !document) return; + const center = (touches: TouchList) => ({ + x: (touches[0].clientX + touches[1].clientX) / 2, + y: (touches[0].clientY + touches[1].clientY) / 2, + }); + const distance = (touches: TouchList) => + Math.hypot(touches[1].clientX - touches[0].clientX, touches[1].clientY - touches[0].clientY); + const begin = (event: TouchEvent) => { + event.preventDefault(); + if (event.touches.length >= 2) { + const point = center(event.touches); + touchRef.current = { mode: "pinch", ...point, distance: distance(event.touches), pan: panRef.current, zoom: zoomRef.current }; + } else if (event.touches.length === 1) { + touchRef.current = { + mode: "pan", + x: event.touches[0].clientX, + y: event.touches[0].clientY, + distance: 0, + pan: panRef.current, + zoom: zoomRef.current, + }; + } + }; + const move = (event: TouchEvent) => { + event.preventDefault(); + const gesture = touchRef.current; + if (!gesture) return; + if (event.touches.length >= 2) { + const point = center(event.touches); + if (gesture.mode !== "pinch") { + touchRef.current = { mode: "pinch", ...point, distance: distance(event.touches), pan: panRef.current, zoom: zoomRef.current }; + return; + } + setZoom(Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, gesture.zoom * distance(event.touches) / gesture.distance))); + setPan({ x: gesture.pan.x + point.x - gesture.x, y: gesture.pan.y + point.y - gesture.y }); + } else if (event.touches.length === 1 && gesture.mode === "pan") { + setPan({ + x: gesture.pan.x + event.touches[0].clientX - gesture.x, + y: gesture.pan.y + event.touches[0].clientY - gesture.y, + }); + } + }; + const end = (event: TouchEvent) => { + event.preventDefault(); + if (event.touches.length) begin(event); + else touchRef.current = null; + }; + canvas.addEventListener("touchstart", begin, { passive: false }); + canvas.addEventListener("touchmove", move, { passive: false }); + canvas.addEventListener("touchend", end, { passive: false }); + canvas.addEventListener("touchcancel", end, { passive: false }); + return () => { + canvas.removeEventListener("touchstart", begin); + canvas.removeEventListener("touchmove", move); + canvas.removeEventListener("touchend", end); + canvas.removeEventListener("touchcancel", end); + }; + }, [document]); const openFile = useCallback(async (file: File) => { setLoading(true); @@ -116,21 +195,25 @@ export default function App() { setActive(index); setZoom(Math.max(MIN_ZOOM, transform.zoom)); setPan(transform.pan); + setSidebarOpen(false); }; return (
-
XD{document.name}{document.version && v{document.version}}
+
+ + XD{document.name}{document.version && v{document.version}} +
- + {Math.round(zoom * 100)}% - + - +
-