Files
Verso/services/web/frontend/js/features/ide-react/components/toolbar/change-layout-options.tsx
T
claude 11227d59e3
Build and Deploy Verso / deploy (push) Successful in 14m3s
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 <noreply@anthropic.com>
2026-06-16 09:25:15 +00:00

197 lines
4.7 KiB
TypeScript

import {
DropdownItem,
DropdownHeader,
} from '@/shared/components/dropdown/dropdown-menu'
import {
IdeLayout,
IdeView,
useLayoutContext,
} from '@/shared/context/layout-context'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { DetachRole } from '@/shared/context/detach-context'
import OLSpinner from '@/shared/components/ol/ol-spinner'
import { isMac } from '@/shared/utils/os'
import { Shortcut } from '@/shared/components/shortcut'
import classNames from 'classnames'
type LayoutOption =
| 'sideBySide'
| 'verticalSplit'
| 'editorOnly'
| 'pdfOnly'
| 'detachedPdf'
const getActiveLayoutOption = ({
pdfLayout,
view,
detachRole,
}: {
pdfLayout: IdeLayout
view: IdeView | null
detachRole?: DetachRole
}): LayoutOption | null => {
if (view === 'history') {
return null
}
if (detachRole === 'detacher') {
return 'detachedPdf'
}
if (pdfLayout === 'flat' && (view === 'editor' || view === 'file')) {
return 'editorOnly'
}
if (pdfLayout === 'flat' && view === 'pdf') {
return 'pdfOnly'
}
if (pdfLayout === 'sideBySide') {
return 'sideBySide'
}
if (pdfLayout === 'verticalSplit') {
return 'verticalSplit'
}
return null
}
const LayoutDropdownItem = ({
active,
disabled = false,
processing = false,
leadingIcon,
trailingIcon,
onClick,
children,
}: {
active: boolean
leadingIcon: React.ReactNode
trailingIcon?: React.ReactNode
onClick: () => void
children: React.ReactNode
processing?: boolean
disabled?: boolean
}) => {
if (processing) {
leadingIcon = <OLSpinner size="sm" />
} else if (active) {
leadingIcon = 'check'
}
return (
<DropdownItem
active={active}
aria-current={active}
disabled={disabled}
onClick={onClick}
leadingIcon={leadingIcon}
trailingIcon={trailingIcon}
className={classNames({ 'dropdown-item-wide': isMac })}
>
{children}
</DropdownItem>
)
}
const shortcuts: Record<LayoutOption, string[] | null> = isMac
? {
editorOnly: ['⌃', '⌘', '←'],
pdfOnly: ['⌃', '⌘', '→'],
sideBySide: ['⌃', '⌘', '↓'],
verticalSplit: null,
detachedPdf: ['⌃', '⌘', '↑'],
}
: {
editorOnly: null,
pdfOnly: null,
sideBySide: null,
verticalSplit: null,
detachedPdf: null,
}
export default function ChangeLayoutOptions() {
const {
detachIsLinked,
detachRole,
view,
pdfLayout,
handleChangeLayout,
handleDetach,
} = useLayoutContext()
const { t } = useTranslation()
const detachable = 'BroadcastChannel' in window
const activeLayoutOption = getActiveLayoutOption({
pdfLayout,
view,
detachRole,
})
const waitingForDetachedLink = !detachIsLinked && detachRole === 'detacher'
return (
<>
<DropdownHeader>{t('layout_options')}</DropdownHeader>
<LayoutDropdownItem
onClick={() => handleChangeLayout('sideBySide')}
active={activeLayoutOption === 'sideBySide'}
leadingIcon="splitscreen_right"
trailingIcon={
shortcuts.sideBySide && <Shortcut keys={shortcuts.sideBySide} />
}
>
{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'}
leadingIcon="edit"
trailingIcon={
shortcuts.editorOnly && <Shortcut keys={shortcuts.editorOnly} />
}
>
{t('editor_only')}
</LayoutDropdownItem>
<LayoutDropdownItem
onClick={() => handleChangeLayout('flat', 'pdf')}
active={activeLayoutOption === 'pdfOnly'}
leadingIcon="picture_as_pdf"
trailingIcon={
shortcuts.pdfOnly && <Shortcut keys={shortcuts.pdfOnly} />
}
>
{t('pdf_only')}
</LayoutDropdownItem>
<LayoutDropdownItem
onClick={() => handleDetach()}
active={activeLayoutOption === 'detachedPdf' && detachIsLinked}
disabled={!detachable}
leadingIcon="open_in_new"
trailingIcon={
shortcuts.detachedPdf && <Shortcut keys={shortcuts.detachedPdf} />
}
processing={waitingForDetachedLink}
>
{t('open_pdf_in_separate_tab')}
</LayoutDropdownItem>
</>
)
}