Editor mobile ergonomics: vertical split layout on phones and as desktop option
Build and Deploy Verso / deploy (push) Successful in 14m3s
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:
@@ -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,6 +105,14 @@ export default function MainLayout() {
|
||||
<EditorPanel />
|
||||
</div>
|
||||
</Panel>
|
||||
{isVertical ? (
|
||||
<VerticalResizeHandle
|
||||
onDragging={setResizing}
|
||||
className={classNames({
|
||||
hidden: !editorIsOpen,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<HorizontalResizeHandle
|
||||
resizable={pdfLayout === 'sideBySide'}
|
||||
onDragging={setResizing}
|
||||
@@ -102,6 +136,7 @@ export default function MainLayout() {
|
||||
</div>
|
||||
)}
|
||||
</HorizontalResizeHandle>
|
||||
)}
|
||||
<Panel
|
||||
collapsible
|
||||
className={classNames('ide-redesign-pdf-container', {
|
||||
|
||||
+24
-1
@@ -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])
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user