@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22import { DEFAULT_BEAD_SYMBOLS } from './domain/defaults'
33import { useEditorStore } from './domain/editorStore'
44import { buildReportSummary } from './domain/report'
5+ import { MAX_ZOOM_INDEX } from './domain/zoom'
56import { useI18n } from './i18n/I18nProvider'
67import type { AppLocale } from './i18n/translations'
78import {
@@ -25,6 +26,7 @@ import { useDocumentFileActions } from './ui/hooks/useDocumentFileActions'
2526import { useEditorShortcuts } from './ui/hooks/useEditorShortcuts'
2627import { useMobilePortraitSinglePane } from './ui/hooks/useMobilePortraitSinglePane'
2728import { usePaneSync } from './ui/hooks/usePaneSync'
29+ import { useCanvasGestures } from './ui/hooks/useCanvasGestures'
2830import { usePointerEditing } from './ui/hooks/usePointerEditing'
2931import { usePointerDismiss } from './ui/hooks/usePointerDismiss'
3032import { useProjectBootstrap } from './ui/hooks/useProjectBootstrap'
@@ -236,7 +238,7 @@ function App() {
236238 const drawColors = document . view . drawColors
237239 const drawSymbols = document . view . drawSymbols
238240 const zoomIndex = document . view . zoom
239- const canZoomIn = zoomIndex < 7
241+ const canZoomIn = zoomIndex < MAX_ZOOM_INDEX
240242 const canZoomOut = zoomIndex > 0
241243 const hasCanvasPaneVisible = isDraftVisible || isCorrectedVisible || isSimulationVisible
242244 const hasAnyPaneVisible = hasCanvasPaneVisible || isReportVisible
@@ -286,6 +288,25 @@ function App() {
286288 setZoom,
287289 } )
288290
291+ const gesturePaneRefs = useMemo (
292+ ( ) => [ draftScrollRef , correctedScrollRef , simulationScrollRef ] ,
293+ [ correctedScrollRef , draftScrollRef , simulationScrollRef ] ,
294+ )
295+
296+ useCanvasGestures ( {
297+ panes : gesturePaneRefs ,
298+ onZoomIn : ( ) => {
299+ setIsZoomFitMode ( false )
300+ zoomIn ( )
301+ } ,
302+ onZoomOut : ( ) => {
303+ setIsZoomFitMode ( false )
304+ zoomOut ( )
305+ } ,
306+ onShiftLeft : ( ) => shiftLeft ( ) ,
307+ onShiftRight : ( ) => shiftRight ( ) ,
308+ } )
309+
289310 const reportSummary = useMemo (
290311 ( ) =>
291312 buildReportSummary ( document , openFileName , {
@@ -447,22 +468,12 @@ function App() {
447468 setIsMetadataDialogOpen ( true )
448469 } , [ document . author , document . notes , document . organization ] )
449470
450- const onSelectMobileView = useCallback (
451- ( pane : ViewPaneId ) => {
452- setViewVisibility ( 'draft' , pane === 'draft' )
453- setViewVisibility ( 'corrected' , pane === 'corrected' )
454- setViewVisibility ( 'simulation' , pane === 'simulation' )
455- setViewVisibility ( 'report' , pane === 'report' )
456- } ,
457- [ setViewVisibility ] ,
458- )
459-
460- useMobilePortraitSinglePane ( {
471+ const { isCompactTabsMode, maxVisiblePanes } = useMobilePortraitSinglePane ( {
461472 isDraftVisible,
462473 isCorrectedVisible,
463474 isSimulationVisible,
464475 isReportVisible,
465- onSelectMobileView ,
476+ onSetPaneVisibility : setViewVisibility ,
466477 } )
467478
468479 const onApplyPreferences = useCallback ( async ( ) => {
@@ -542,13 +553,55 @@ function App() {
542553 simulation : isSimulationVisible ,
543554 report : isReportVisible ,
544555 }
545- const mobileActivePane : ViewPaneId = isDraftVisible
546- ? 'draft'
547- : isCorrectedVisible
548- ? 'corrected'
549- : isSimulationVisible
550- ? 'simulation'
551- : 'report'
556+ const onToggleMobileView = useCallback (
557+ ( pane : ViewPaneId ) => {
558+ const visibleByPane : Record < ViewPaneId , boolean > = {
559+ draft : isDraftVisible ,
560+ corrected : isCorrectedVisible ,
561+ simulation : isSimulationVisible ,
562+ report : isReportVisible ,
563+ }
564+ const orderedPanes : ViewPaneId [ ] = [ 'draft' , 'corrected' , 'simulation' , 'report' ]
565+ const visiblePanes = orderedPanes . filter ( ( id ) => visibleByPane [ id ] )
566+ const isVisible = visibleByPane [ pane ]
567+
568+ if ( ! isCompactTabsMode ) {
569+ // Desktop behavior: open one pane and keep others unchanged is confusing.
570+ // Keep mobile tab interactions no-op outside compact mode.
571+ return
572+ }
573+
574+ if ( isVisible ) {
575+ if ( visiblePanes . length <= 1 ) {
576+ return
577+ }
578+ setViewVisibility ( pane , false )
579+ return
580+ }
581+
582+ if ( maxVisiblePanes === 1 ) {
583+ orderedPanes . forEach ( ( id ) => {
584+ setViewVisibility ( id , id === pane )
585+ } )
586+ return
587+ }
588+
589+ if ( visiblePanes . length >= maxVisiblePanes ) {
590+ // Keep the latest selection by dropping the first visible pane.
591+ setViewVisibility ( visiblePanes [ 0 ] , false )
592+ }
593+ setViewVisibility ( pane , true )
594+ } ,
595+ [
596+ isCompactTabsMode ,
597+ isCorrectedVisible ,
598+ isDraftVisible ,
599+ isReportVisible ,
600+ isSimulationVisible ,
601+ maxVisiblePanes ,
602+ setViewVisibility ,
603+ ] ,
604+ )
552605
553606 const paneLabels = useMemo (
554607 ( ) => ( {
@@ -634,13 +687,13 @@ function App() {
634687 hasAnyPaneVisible = { hasAnyPaneVisible }
635688 recentFilesCount = { recentFiles . length }
636689 isMobileActionsMenuOpen = { isMobileActionsMenuOpen }
637- mobileActivePane = { mobileActivePane }
690+ mobilePaneVisibilityById = { paneVisibilityById }
638691 panes = { panes }
639692 mobileActionsMenuRef = { mobileActionsMenuRef }
640693 openFileInputRef = { openFileInputRef }
641694 onToggleMobileActionsMenu = { ( ) => setIsMobileActionsMenuOpen ( ( value ) => ! value ) }
642695 onCloseMobileActionsMenu = { ( ) => setIsMobileActionsMenuOpen ( false ) }
643- onSelectMobileView = { onSelectMobileView }
696+ onToggleMobileView = { onToggleMobileView }
644697 onNewDocument = { onNewDocument }
645698 onOpenDocument = { onOpenDocument }
646699 onOpenRecentDialog = { onOpenRecentDialog }
0 commit comments