Editor mobile ergonomics: vertical split layout on phones and as desktop option
Build and Deploy Verso / deploy (push) Successful in 14m3s

On mobile (< 768 px) the existing side-by-side layout automatically switches
to a vertical stack (editor on top, PDF/presentation on bottom) without
changing the stored layout preference.

A new "Top / bottom split" option is added to the layout menu so desktop
users can choose the same vertical split explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-16 09:25:15 +00:00
parent 0f585ea5bb
commit 11227d59e3
8 changed files with 106 additions and 35 deletions
@@ -1,6 +1,7 @@
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'
@@ -8,7 +9,7 @@ import { HorizontalToggler } from '@/features/ide-react/components/resize/horizo
import { useTranslation } from 'react-i18next'
import { usePdfPane } from '@/features/ide-react/hooks/use-pdf-pane'
import { useLayoutContext } from '@/shared/context/layout-context'
import { ElementType, useState } from 'react'
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'
@@ -25,6 +26,9 @@ const mainEditorLayoutModalsModules: Array<{
path: string
}> = importOverleafModules('mainEditorLayoutModals')
// Bootstrap md breakpoint — below this we stack panels vertically on mobile
const MOBILE_MQ = '(max-width: 767px)'
export default function MainLayout() {
const [resizing, setResizing] = useState(false)
const { resizing: railResizing } = useRailContext()
@@ -38,8 +42,26 @@ export default function MainLayout() {
} = usePdfPane()
const { view, pdfLayout } = useLayoutContext()
const [isMobile, setIsMobile] = useState(
() => window.matchMedia(MOBILE_MQ).matches
)
useEffect(() => {
const mq = window.matchMedia(MOBILE_MQ)
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches)
mq.addEventListener('change', handler)
return () => mq.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'
view === 'editor' ||
view === 'file' ||
pdfLayout === 'sideBySide' ||
pdfLayout === 'verticalSplit'
const { t } = useTranslation()
@@ -58,8 +80,12 @@ export default function MainLayout() {
<Panel id="ide-redesign-editor-and-pdf-panel" order={2}>
<HistoryContainer />
<PanelGroup
autoSaveId="ide-redesign-editor-and-pdf-panel-group"
direction="horizontal"
autoSaveId={
pdfLayout === 'verticalSplit'
? 'ide-redesign-editor-and-pdf-panel-group-vertical'
: 'ide-redesign-editor-and-pdf-panel-group'
}
direction={isVertical ? 'vertical' : 'horizontal'}
className={classNames({
hidden: view === 'history',
})}
@@ -79,29 +105,38 @@ export default function MainLayout() {
<EditorPanel />
</div>
</Panel>
<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')}
{isVertical ? (
<VerticalResizeHandle
onDragging={setResizing}
className={classNames({
hidden: !editorIsOpen,
})}
/>
{pdfLayout === 'sideBySide' && (
<div className="synctex-controls">
<DefaultSynctexControl />
</div>
)}
</HorizontalResizeHandle>
) : (
<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', {
@@ -15,7 +15,12 @@ import { isMac } from '@/shared/utils/os'
import { Shortcut } from '@/shared/components/shortcut'
import classNames from 'classnames'
type LayoutOption = 'sideBySide' | 'editorOnly' | 'pdfOnly' | 'detachedPdf'
type LayoutOption =
| 'sideBySide'
| 'verticalSplit'
| 'editorOnly'
| 'pdfOnly'
| 'detachedPdf'
const getActiveLayoutOption = ({
pdfLayout,
@@ -46,6 +51,10 @@ const getActiveLayoutOption = ({
return 'sideBySide'
}
if (pdfLayout === 'verticalSplit') {
return 'verticalSplit'
}
return null
}
@@ -92,12 +101,14 @@ const shortcuts: Record<LayoutOption, string[] | null> = isMac
editorOnly: ['⌃', '⌘', '←'],
pdfOnly: ['⌃', '⌘', '→'],
sideBySide: ['⌃', '⌘', '↓'],
verticalSplit: null,
detachedPdf: ['⌃', '⌘', '↑'],
}
: {
editorOnly: null,
pdfOnly: null,
sideBySide: null,
verticalSplit: null,
detachedPdf: null,
}
@@ -136,6 +147,18 @@ export default function ChangeLayoutOptions() {
>
{t('split_view')}
</LayoutDropdownItem>
<LayoutDropdownItem
onClick={() => handleChangeLayout('verticalSplit')}
active={activeLayoutOption === 'verticalSplit'}
leadingIcon="horizontal_split"
trailingIcon={
shortcuts.verticalSplit && (
<Shortcut keys={shortcuts.verticalSplit} />
)
}
>
{t('top_bottom_split_view')}
</LayoutDropdownItem>
<LayoutDropdownItem
onClick={() => handleChangeLayout('flat', 'editor')}
active={activeLayoutOption === 'editorOnly'}
@@ -8,7 +8,8 @@ export const usePdfPane = () => {
useLayoutContext()
const pdfPanelRef = useRef<ImperativePanelHandle>(null)
const pdfIsOpen = pdfLayout === 'sideBySide' || view === 'pdf'
const pdfIsOpen =
pdfLayout === 'sideBySide' || pdfLayout === 'verticalSplit' || view === 'pdf'
useCollapsiblePanel(pdfIsOpen, pdfPanelRef)
@@ -46,7 +47,7 @@ export const usePdfPane = () => {
// triggered when the PDF pane becomes closed (either by dragging or toggling)
const handlePdfPaneCollapse = useCallback(() => {
if (pdfLayout === 'sideBySide') {
if (pdfLayout === 'sideBySide' || pdfLayout === 'verticalSplit') {
changeLayout('flat', 'editor')
}
}, [changeLayout, pdfLayout])
@@ -12,7 +12,7 @@ function SwitchToEditorButton() {
return null
}
if (pdfLayout === 'sideBySide') {
if (pdfLayout === 'sideBySide' || pdfLayout === 'verticalSplit') {
return null
}
@@ -12,7 +12,7 @@ function SwitchToPDFButton() {
return null
}
if (pdfLayout === 'sideBySide') {
if (pdfLayout === 'sideBySide' || pdfLayout === 'verticalSplit') {
return null
}
@@ -25,7 +25,7 @@ import usePersistedState from '@/shared/hooks/use-persisted-state'
import { repositionAllTooltips } from '@/features/source-editor/extensions/tooltips-reposition'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
export type IdeLayout = 'sideBySide' | 'flat'
export type IdeLayout = 'sideBySide' | 'flat' | 'verticalSplit'
export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
export type LayoutContextOwnStates = {
@@ -77,7 +77,11 @@ export const LayoutContext = createContext<LayoutContextValue | undefined>(
function setLayoutInLocalStorage(pdfLayout: IdeLayout) {
localStorage.setItem(
'pdf.layout',
pdfLayout === 'sideBySide' ? 'split' : 'flat'
pdfLayout === 'sideBySide'
? 'split'
: pdfLayout === 'verticalSplit'
? 'vertical'
: 'flat'
)
}
@@ -199,7 +203,10 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
const changeLayout = useCallback(
(newLayout: IdeLayout, newView: IdeView = 'editor') => {
const targetView = newLayout === 'sideBySide' ? 'editor' : newView
const targetView =
newLayout === 'sideBySide' || newLayout === 'verticalSplit'
? 'editor'
: newView
setPdfLayout(newLayout)
if (targetView === 'editor') {
restoreView()
@@ -230,7 +237,10 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
} = useDetachLayout()
const pdfPreviewOpen =
pdfLayout === 'sideBySide' || view === 'pdf' || detachRole === 'detacher'
pdfLayout === 'sideBySide' ||
pdfLayout === 'verticalSplit' ||
view === 'pdf' ||
detachRole === 'detacher'
useEffect(() => {
if (debugPdfDetach) {
+1
View File
@@ -2485,6 +2485,7 @@
"spellcheck_language": "Spellcheck language",
"spelling_and_language": "Spelling and language",
"split_view": "Split view",
"top_bottom_split_view": "Top / bottom split",
"sso": "SSO",
"sso_account_already_linked": "Account already linked to another __appName__ user",
"sso_active": "SSO active",
+1
View File
@@ -2490,6 +2490,7 @@
"spellcheck_language": "Langue de vérification orthographique",
"spelling_and_language": "Orthographe et langue",
"split_view": "Vue fractionnée",
"top_bottom_split_view": "Vue haute / basse",
"sso": "SSO",
"sso_account_already_linked": "Compte déjà lié à un·e autre utilisateur·rice __appName__",
"sso_active": "SSO actif",