From 11227d59e3631b77a77f41ae3eb59fcb326cac10 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 16 Jun 2026 09:25:15 +0000 Subject: [PATCH] Editor mobile ergonomics: vertical split layout on phones and as desktop option 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 --- .../components/layout/main-layout.tsx | 87 +++++++++++++------ .../toolbar/change-layout-options.tsx | 25 +++++- .../features/ide-react/hooks/use-pdf-pane.ts | 5 +- .../components/switch-to-editor-button.tsx | 2 +- .../components/switch-to-pdf-button.tsx | 2 +- .../js/shared/context/layout-context.tsx | 18 +++- services/web/locales/en.json | 1 + services/web/locales/fr.json | 1 + 8 files changed, 106 insertions(+), 35 deletions(-) diff --git a/services/web/frontend/js/features/ide-react/components/layout/main-layout.tsx b/services/web/frontend/js/features/ide-react/components/layout/main-layout.tsx index a358f2c821..a14f82b6ee 100644 --- a/services/web/frontend/js/features/ide-react/components/layout/main-layout.tsx +++ b/services/web/frontend/js/features/ide-react/components/layout/main-layout.tsx @@ -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() { - - - {pdfLayout === 'sideBySide' && ( -
- -
- )} -
+ ) : ( + + + {pdfLayout === 'sideBySide' && ( +
+ +
+ )} +
+ )} = 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')} + handleChangeLayout('verticalSplit')} + active={activeLayoutOption === 'verticalSplit'} + leadingIcon="horizontal_split" + trailingIcon={ + shortcuts.verticalSplit && ( + + ) + } + > + {t('top_bottom_split_view')} + handleChangeLayout('flat', 'editor')} active={activeLayoutOption === 'editorOnly'} diff --git a/services/web/frontend/js/features/ide-react/hooks/use-pdf-pane.ts b/services/web/frontend/js/features/ide-react/hooks/use-pdf-pane.ts index 0a232edcbe..e21026ae70 100644 --- a/services/web/frontend/js/features/ide-react/hooks/use-pdf-pane.ts +++ b/services/web/frontend/js/features/ide-react/hooks/use-pdf-pane.ts @@ -8,7 +8,8 @@ export const usePdfPane = () => { useLayoutContext() const pdfPanelRef = useRef(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]) diff --git a/services/web/frontend/js/features/pdf-preview/components/switch-to-editor-button.tsx b/services/web/frontend/js/features/pdf-preview/components/switch-to-editor-button.tsx index 7eaa4bfb3d..503870a198 100644 --- a/services/web/frontend/js/features/pdf-preview/components/switch-to-editor-button.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/switch-to-editor-button.tsx @@ -12,7 +12,7 @@ function SwitchToEditorButton() { return null } - if (pdfLayout === 'sideBySide') { + if (pdfLayout === 'sideBySide' || pdfLayout === 'verticalSplit') { return null } diff --git a/services/web/frontend/js/features/source-editor/components/switch-to-pdf-button.tsx b/services/web/frontend/js/features/source-editor/components/switch-to-pdf-button.tsx index 1cd2a4140a..2bd8cefab1 100644 --- a/services/web/frontend/js/features/source-editor/components/switch-to-pdf-button.tsx +++ b/services/web/frontend/js/features/source-editor/components/switch-to-pdf-button.tsx @@ -12,7 +12,7 @@ function SwitchToPDFButton() { return null } - if (pdfLayout === 'sideBySide') { + if (pdfLayout === 'sideBySide' || pdfLayout === 'verticalSplit') { return null } diff --git a/services/web/frontend/js/shared/context/layout-context.tsx b/services/web/frontend/js/shared/context/layout-context.tsx index 821834a9ac..32eae290ab 100644 --- a/services/web/frontend/js/shared/context/layout-context.tsx +++ b/services/web/frontend/js/shared/context/layout-context.tsx @@ -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( 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 = ({ 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 = ({ children }) => { } = useDetachLayout() const pdfPreviewOpen = - pdfLayout === 'sideBySide' || view === 'pdf' || detachRole === 'detacher' + pdfLayout === 'sideBySide' || + pdfLayout === 'verticalSplit' || + view === 'pdf' || + detachRole === 'detacher' useEffect(() => { if (debugPdfDetach) { diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 12246a6fca..bae3a39fc7 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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", diff --git a/services/web/locales/fr.json b/services/web/locales/fr.json index a37a99174b..8e7c5908a1 100644 --- a/services/web/locales/fr.json +++ b/services/web/locales/fr.json @@ -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",