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",