@@ -71,6 +71,7 @@ export function Excalidraw({
7171
7272 // The raw viewBox of the entire exported SVG
7373 const [ rawViewBox , setRawViewBox ] = useState < number [ ] | null > ( null ) ;
74+ const overviewTargetRef = useRef < number [ ] | null > ( null ) ;
7475 const currentViewBoxRef = useRef < number [ ] > ( [ 0 , 0 , 100 , 100 ] ) ;
7576
7677 const requestRef = useRef < number | null > ( null ) ;
@@ -138,12 +139,21 @@ export function Excalidraw({
138139 ${ katexStyles }
139140 .katex-display { margin: 0; }
140141 .katex { font-size: 1.1em; }
142+ /* Fallback for mathematical symbols like integrals when KaTeX fonts are not loaded in SVG data URLs */
143+ .katex-mathml { display: none; }
144+ .katex-html { font-family: KaTeX_Main, "Times New Roman", serif; }
145+ .base { font-family: inherit; }
146+ /* Target specific math symbols to use system math fonts if available */
147+ .mop { font-family: "Cambria Math", "STIX Math", "Segoe UI Symbol", "Apple Symbols", serif !important; }
141148 </style>
142149 ${ html }
143150 </div>
144151 </foreignObject>
145152</svg>` . trim ( ) ;
146- const dataURL = `data:image/svg+xml;base64,${ btoa ( unescape ( encodeURIComponent ( svgString ) ) ) } ` ;
153+ // Use modern TextEncoder instead of deprecated unescape for Base64 encoding
154+ const bytes = new TextEncoder ( ) . encode ( svgString ) ;
155+ const binString = Array . from ( bytes , ( byte ) => String . fromCharCode ( byte ) ) . join ( "" ) ;
156+ const dataURL = `data:image/svg+xml;base64,${ btoa ( binString ) } ` ;
147157 json . files [ id ] = {
148158 mimeType : "image/svg+xml" ,
149159 id,
@@ -182,12 +192,12 @@ export function Excalidraw({
182192 if ( svgRef . current ) {
183193 svgRef . current . setAttribute ( 'viewBox' , vb . join ( ' ' ) ) ;
184194 currentViewBoxRef . current = vb ;
185- if ( rawViewBox ) {
186- const z = Math . round ( ( rawViewBox [ 2 ] / vb [ 2 ] ) * 100 ) ;
195+ if ( overviewTargetRef . current ) {
196+ const z = Math . round ( ( overviewTargetRef . current [ 2 ] / vb [ 2 ] ) * 100 ) ;
187197 setZoom ( z ) ;
188198 }
189199 }
190- } , [ rawViewBox ] ) ;
200+ } , [ ] ) ;
191201
192202 // 2. Render SVG
193203 useEffect ( ( ) => {
@@ -305,6 +315,10 @@ export function Excalidraw({
305315 const vb = svg . getAttribute ( 'viewBox' ) ?. split ( ' ' ) . map ( parseFloat ) ;
306316 if ( vb && vb . length === 4 ) {
307317 setRawViewBox ( vb ) ;
318+ // Use initial overviewTargetRef to keep zoom reference stable
319+ if ( ! overviewTargetRef . current && viewMode === 'overview' ) {
320+ overviewTargetRef . current = vb ;
321+ }
308322 currentViewBoxRef . current = vb ;
309323 setCoordinateOffset ( { x : vb [ 0 ] - minX , y : vb [ 1 ] - minY } ) ;
310324 }
@@ -319,7 +333,7 @@ export function Excalidraw({
319333 }
320334 }
321335 renderSvg ( ) ;
322- } , [ data , viewMode ] ) ;
336+ } , [ data , viewMode , fontFamily ] ) ;
323337
324338 const animate = useCallback ( ( time : number ) => {
325339 if ( ! transitionRef . current ) return ;
@@ -342,11 +356,11 @@ export function Excalidraw({
342356
343357 // Sync View Logic
344358 const syncView = useCallback ( ( ) => {
345- if ( ! rawViewBox ) return ;
359+ if ( ! rawViewBox || ! overviewTargetRef . current ) return ;
346360
347361 let target : number [ ] ;
348362 if ( viewMode === 'overview' ) {
349- target = rawViewBox ;
363+ target = overviewTargetRef . current ;
350364 } else {
351365 const frame = frames [ currentSlide ] ;
352366 if ( frame ) {
@@ -359,7 +373,7 @@ export function Excalidraw({
359373 frame . height + p * 2
360374 ] ;
361375 } else {
362- target = rawViewBox ;
376+ target = overviewTargetRef . current ;
363377 }
364378 }
365379 updateCamera ( target ) ;
@@ -394,9 +408,14 @@ export function Excalidraw({
394408 return ( ) => window . removeEventListener ( 'keydown' , handleKeyDown ) ;
395409 } , [ viewMode , currentSlide , frames . length ] ) ;
396410
397- // Drag Handlers
398- const handleMouseDown = ( e : React . MouseEvent ) => {
399- e . preventDefault ( ) ;
411+ // Unified Pointer Drag Handlers (Mouse, Touch, Pen)
412+ const handlePointerDown = ( e : React . PointerEvent ) => {
413+ // Only handle primary pointer (usually left click or first touch)
414+ if ( ! e . isPrimary ) return ;
415+
416+ // Capture pointer so we continue receiving events even if the pointer leaves the container
417+ e . currentTarget . setPointerCapture ( e . pointerId ) ;
418+
400419 setIsDragging ( true ) ;
401420 dragStartRef . current = {
402421 x : e . clientX ,
@@ -407,7 +426,7 @@ export function Excalidraw({
407426 if ( requestRef . current ) cancelAnimationFrame ( requestRef . current ) ;
408427 } ;
409428
410- const handleMouseMove = ( e : React . MouseEvent ) => {
429+ const handlePointerMove = ( e : React . PointerEvent ) => {
411430 if ( ! isDragging || ! dragStartRef . current || ! containerRef . current ) return ;
412431
413432 const rect = containerRef . current . getBoundingClientRect ( ) ;
@@ -417,10 +436,6 @@ export function Excalidraw({
417436 const dy = e . clientY - dragStartRef . current . y ;
418437
419438 const [ vx , vy , vw , vh ] = dragStartRef . current . vb ;
420-
421- // Calculate scale. Since we use 'meet', the SVG scale matches the constraining dimension.
422- // We use the MAX dimension ratio to approximate the zoom level for panning speed.
423- // This ensures 1px of drag ~= 1px of SVG movement regardless of aspect ratio letterboxing.
424439 const scale = Math . max ( vw / rect . width , vh / rect . height ) ;
425440
426441 const newVB = [
@@ -433,9 +448,13 @@ export function Excalidraw({
433448 setSvgViewBox ( newVB ) ;
434449 } ;
435450
436- const handleMouseUp = ( ) => {
451+ const handlePointerUp = ( e : React . PointerEvent ) => {
437452 setIsDragging ( false ) ;
438453 dragStartRef . current = null ;
454+ // Release is automatic but good practice to clear state
455+ if ( e . currentTarget . hasPointerCapture ( e . pointerId ) ) {
456+ e . currentTarget . releasePointerCapture ( e . pointerId ) ;
457+ }
439458 } ;
440459
441460 // Zoom Handler
@@ -461,7 +480,13 @@ export function Excalidraw({
461480 return ( ) => el . removeEventListener ( 'wheel' , onWheelPassive ) ;
462481 } , [ loading , setSvgViewBox ] ) ;
463482
464- const containerStyle = { height : typeof height === 'number' ? `${ height } px` : height , width : typeof width === 'number' ? `${ width } px` : width } ;
483+ const containerStyle = {
484+ height : typeof height === 'number' ? `${ height } px` : height ,
485+ maxHeight : '70vh' ,
486+ minHeight : '300px' ,
487+ width : typeof width === 'number' ? `${ width } px` : width ,
488+ touchAction : 'none' // Prevent native browser gestures (scroll/pinch) from interfering
489+ } ;
465490
466491 if ( ! isClient ) return < div style = { containerStyle } className = "bg-gray-50 flex items-center justify-center border-2 border-gray-200 rounded-xl" > Initializing...</ div > ;
467492
@@ -482,10 +507,11 @@ export function Excalidraw({
482507 className = { `relative bg-white overflow-hidden select-none group ${ isDragging ? 'cursor-grabbing' : 'cursor-grab' } ` }
483508 style = { containerStyle }
484509 tabIndex = { 0 }
485- onMouseDown = { handleMouseDown }
486- onMouseMove = { handleMouseMove }
487- onMouseUp = { handleMouseUp }
488- onMouseLeave = { handleMouseUp }
510+ onPointerDown = { handlePointerDown }
511+ onPointerMove = { handlePointerMove }
512+ onPointerUp = { handlePointerUp }
513+ onPointerCancel = { handlePointerUp }
514+ onPointerLeave = { handlePointerUp }
489515 >
490516 < div ref = { svgContainerRef } className = "absolute inset-0 w-full h-full pointer-events-none" />
491517
0 commit comments