51d0314c1c
Build and Deploy Verso / deploy (push) Successful in 10m17s
Tor Browser and Firefox with privacy.resistFingerprinting report a
desktop-sized viewport (~980px) even on a real Android phone. This made
window.matchMedia('(max-width: 767px)') return false, so isMobile was
always false on Tor, leaving the editor in side-by-side (horizontal)
layout instead of the expected vertical stack.
Fix: add a secondary check using `(pointer: coarse) and (max-width:
1024px)`. Touch hardware is not spoofed by fingerprinting resistance,
so this reliably catches phones and tablets regardless of the reported
viewport width. Applied to both getInitialLayout() and the live
isMobile state in main-layout.tsx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
200 lines
7.3 KiB
TypeScript
200 lines
7.3 KiB
TypeScript
import { Panel, PanelGroup } from 'react-resizable-panels'
|
|
import classNames from 'classnames'
|
|
import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
|
|
import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
|
|
import PdfPreview from '@/features/pdf-preview/components/pdf-preview'
|
|
import { RailLayout } from '../rail/rail'
|
|
import { Toolbar } from '../toolbar/toolbar'
|
|
import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { usePdfPane } from '@/features/ide-react/hooks/use-pdf-pane'
|
|
import { useLayoutContext } from '@/shared/context/layout-context'
|
|
import { ElementType, useEffect, useState } from 'react'
|
|
import EditorPanel from '../editor/editor-panel'
|
|
import { useRailContext } from '../../context/rail-context'
|
|
import HistoryContainer from '@/features/ide-react/components/history-container'
|
|
import { DefaultSynctexControl } from '@/features/pdf-preview/components/detach-synctex-control'
|
|
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
|
|
|
const mainEditorLayoutPanels: Array<{
|
|
import: { default: ElementType }
|
|
path: string
|
|
}> = importOverleafModules('mainEditorLayoutPanels')
|
|
|
|
const mainEditorLayoutModalsModules: Array<{
|
|
import: { default: ElementType }
|
|
path: string
|
|
}> = importOverleafModules('mainEditorLayoutModals')
|
|
|
|
// Bootstrap md breakpoint — below this we stack panels vertically on mobile
|
|
const MOBILE_MQ = '(max-width: 767px)'
|
|
// Secondary check: browsers that spoof viewport width (e.g. Tor Browser) still
|
|
// expose `pointer: coarse` for real touch hardware.
|
|
const TOUCH_MQ = '(pointer: coarse) and (max-width: 1024px)'
|
|
|
|
function detectMobile() {
|
|
return (
|
|
window.matchMedia(MOBILE_MQ).matches ||
|
|
window.matchMedia(TOUCH_MQ).matches
|
|
)
|
|
}
|
|
|
|
export default function MainLayout() {
|
|
const [resizing, setResizing] = useState(false)
|
|
const { resizing: railResizing } = useRailContext()
|
|
const {
|
|
togglePdfPane,
|
|
handlePdfPaneExpand,
|
|
handlePdfPaneCollapse,
|
|
setPdfIsOpen: setIsPdfOpen,
|
|
pdfIsOpen: isPdfOpen,
|
|
pdfPanelRef,
|
|
} = usePdfPane()
|
|
const { view, pdfLayout } = useLayoutContext()
|
|
|
|
const [isMobile, setIsMobile] = useState(detectMobile)
|
|
useEffect(() => {
|
|
const handler = () => setIsMobile(detectMobile())
|
|
const mq1 = window.matchMedia(MOBILE_MQ)
|
|
const mq2 = window.matchMedia(TOUCH_MQ)
|
|
mq1.addEventListener('change', handler)
|
|
mq2.addEventListener('change', handler)
|
|
return () => {
|
|
mq1.removeEventListener('change', handler)
|
|
mq2.removeEventListener('change', handler)
|
|
}
|
|
}, [])
|
|
|
|
// verticalSplit is always vertical; sideBySide becomes vertical on mobile
|
|
const isVertical =
|
|
pdfLayout === 'verticalSplit' ||
|
|
(pdfLayout === 'sideBySide' && isMobile)
|
|
|
|
const editorIsOpen =
|
|
view === 'editor' ||
|
|
view === 'file' ||
|
|
pdfLayout === 'sideBySide' ||
|
|
pdfLayout === 'verticalSplit'
|
|
|
|
const { t } = useTranslation()
|
|
|
|
return (
|
|
<div className="ide-redesign-main">
|
|
<Toolbar />
|
|
<div className="ide-redesign-body">
|
|
<PanelGroup
|
|
autoSaveId="ide-redesign-outer-layout"
|
|
direction="horizontal"
|
|
className={classNames('ide-redesign-inner', {
|
|
'ide-panel-group-resizing': resizing || railResizing,
|
|
})}
|
|
>
|
|
<RailLayout />
|
|
<Panel id="ide-redesign-editor-and-pdf-panel" order={2}>
|
|
<HistoryContainer />
|
|
<PanelGroup
|
|
key={isVertical ? 'vertical' : 'horizontal'}
|
|
autoSaveId={
|
|
isVertical
|
|
? // On mobile, skip autoSave: a stale collapsed PDF pane
|
|
// would fire onCollapse → changeLayout('flat'), overriding
|
|
// the verticalSplit default from getInitialLayout().
|
|
// The pdf.layout localStorage key already persists the
|
|
// user's explicit flat/open preference independently.
|
|
isMobile
|
|
? null
|
|
: 'ide-redesign-editor-and-pdf-panel-group-vertical'
|
|
: 'ide-redesign-editor-and-pdf-panel-group'
|
|
}
|
|
direction={isVertical ? 'vertical' : 'horizontal'}
|
|
className={classNames({
|
|
hidden: view === 'history',
|
|
})}
|
|
>
|
|
<Panel
|
|
id="ide-redesign-editor-panel"
|
|
order={1}
|
|
className={classNames({
|
|
hidden: !editorIsOpen || view === 'history',
|
|
})}
|
|
minSize={5}
|
|
defaultSize={50}
|
|
tagName="section"
|
|
aria-label={t('editor')}
|
|
>
|
|
<div className="ide-redesign-editor-container">
|
|
<EditorPanel />
|
|
</div>
|
|
</Panel>
|
|
{isVertical ? (
|
|
<VerticalResizeHandle
|
|
onDragging={setResizing}
|
|
className={classNames({
|
|
hidden: !editorIsOpen,
|
|
})}
|
|
/>
|
|
) : (
|
|
<HorizontalResizeHandle
|
|
resizable={pdfLayout === 'sideBySide'}
|
|
onDragging={setResizing}
|
|
onDoubleClick={togglePdfPane}
|
|
hitAreaMargins={{ coarse: 0, fine: 0 }}
|
|
className={classNames({
|
|
hidden: !editorIsOpen,
|
|
})}
|
|
>
|
|
<HorizontalToggler
|
|
id="ide-redesign-pdf-panel"
|
|
togglerType="east"
|
|
isOpen={isPdfOpen}
|
|
setIsOpen={setIsPdfOpen}
|
|
tooltipWhenOpen={t('tooltip_hide_pdf')}
|
|
tooltipWhenClosed={t('tooltip_show_pdf')}
|
|
/>
|
|
{pdfLayout === 'sideBySide' && (
|
|
<div className="synctex-controls">
|
|
<DefaultSynctexControl />
|
|
</div>
|
|
)}
|
|
</HorizontalResizeHandle>
|
|
)}
|
|
<Panel
|
|
collapsible
|
|
className={classNames('ide-redesign-pdf-container', {
|
|
hidden: view === 'history',
|
|
})}
|
|
id="ide-redesign-pdf-panel"
|
|
order={2}
|
|
defaultSize={50}
|
|
minSize={5}
|
|
ref={pdfPanelRef}
|
|
onExpand={handlePdfPaneExpand}
|
|
onCollapse={handlePdfPaneCollapse}
|
|
tagName="section"
|
|
aria-label={t('pdf_preview')}
|
|
>
|
|
<PdfPreview />
|
|
{pdfLayout === 'flat' && view === 'pdf' && (
|
|
<div className="synctex-controls" hidden>
|
|
<DefaultSynctexControl />
|
|
</div>
|
|
)}
|
|
</Panel>
|
|
</PanelGroup>
|
|
</Panel>
|
|
{mainEditorLayoutPanels.map(
|
|
({ import: { default: Component }, path }, i) => {
|
|
return <Component key={path} order={i + 3} />
|
|
}
|
|
)}
|
|
</PanelGroup>
|
|
</div>
|
|
{mainEditorLayoutModalsModules.map(
|
|
({ import: { default: Component }, path }) => (
|
|
<Component key={path} />
|
|
)
|
|
)}
|
|
</div>
|
|
)
|
|
}
|