mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
[web] Add a focus mode to the editor (#32245)
* Basic focus mode implementation * Exit button * Toggle between editor pdf mode * Keep the rail and file tree mounted for editor tabs to work properly * Hide Switch to editor button in focus mode * Show shortcuts in layout menu * Gate the feature behind a focus-mode feature flag * Add split test badges in the focus mode dropdown item and top bar * Add analytics tracking for focusMode changes * Cleanup leftover IdeView value * Allow passing focusMode value in layout test provider * Remove redundant listener for focus mode shortcut * Update focus mode and full project search listeners to use the event key * Post-rebase cleanup --------- Co-authored-by: Anna Fields <acfields11@gmail.com> GitOrigin-RevId: 207f8d136fa6662558c474cca4a867a9842191e7
This commit is contained in:
committed by
Copybot
co-authored by
Anna Fields
parent
757735b075
commit
c8a3bdd13f
@@ -487,6 +487,7 @@ const _ProjectController = {
|
|||||||
'command-palette',
|
'command-palette',
|
||||||
'overleaf-library',
|
'overleaf-library',
|
||||||
'compile-timeout-cta',
|
'compile-timeout-cta',
|
||||||
|
'focus-mode',
|
||||||
'editor-upgrade-button-relocation',
|
'editor-upgrade-button-relocation',
|
||||||
'markdown-visual',
|
'markdown-visual',
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|||||||
@@ -645,6 +645,7 @@
|
|||||||
"event_type": "",
|
"event_type": "",
|
||||||
"example_project": "",
|
"example_project": "",
|
||||||
"existing_plan_active_until_term_end": "",
|
"existing_plan_active_until_term_end": "",
|
||||||
|
"exit_focus_mode": "",
|
||||||
"expand": "",
|
"expand": "",
|
||||||
"experiment_enabled_refresh_page": "",
|
"experiment_enabled_refresh_page": "",
|
||||||
"experiment_full_check_back_soon": "",
|
"experiment_full_check_back_soon": "",
|
||||||
@@ -705,6 +706,7 @@
|
|||||||
"fix_errors": "",
|
"fix_errors": "",
|
||||||
"fixed_width": "",
|
"fixed_width": "",
|
||||||
"fixed_width_wrap_text": "",
|
"fixed_width_wrap_text": "",
|
||||||
|
"focus_mode": "",
|
||||||
"fold_line": "",
|
"fold_line": "",
|
||||||
"folder_location": "",
|
"folder_location": "",
|
||||||
"folder_name": "",
|
"folder_name": "",
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ export default function RailPanel({
|
|||||||
isReviewPanelOpen,
|
isReviewPanelOpen,
|
||||||
isHistoryView,
|
isHistoryView,
|
||||||
railTabs,
|
railTabs,
|
||||||
|
focusMode,
|
||||||
}: {
|
}: {
|
||||||
isReviewPanelOpen: boolean
|
isReviewPanelOpen: boolean
|
||||||
isHistoryView: boolean
|
isHistoryView: boolean
|
||||||
railTabs: RailElement[]
|
railTabs: RailElement[]
|
||||||
|
focusMode: boolean
|
||||||
}) {
|
}) {
|
||||||
const { selectedTab, panelRef, handlePaneExpand, handlePaneCollapse } =
|
const { selectedTab, panelRef, handlePaneExpand, handlePaneCollapse } =
|
||||||
useRailContext()
|
useRailContext()
|
||||||
@@ -35,7 +37,7 @@ export default function RailPanel({
|
|||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
id={`ide-redesign-sidebar-panel-${isHistoryView ? 'file-tree' : selectedTab}`}
|
id={`ide-redesign-sidebar-panel-${isHistoryView ? 'file-tree' : selectedTab}`}
|
||||||
className={classNames({ hidden: isReviewPanelOpen })}
|
className={classNames({ hidden: isReviewPanelOpen || focusMode })}
|
||||||
order={1}
|
order={1}
|
||||||
defaultSize={selectedTab === 'workbench' ? 20 : 15}
|
defaultSize={selectedTab === 'workbench' ? 20 : 15}
|
||||||
minSize={5}
|
minSize={5}
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
export default function RailResizeHandle({
|
export default function RailResizeHandle({
|
||||||
isReviewPanelOpen,
|
isReviewPanelOpen,
|
||||||
|
focusMode,
|
||||||
}: {
|
}: {
|
||||||
isReviewPanelOpen: boolean
|
isReviewPanelOpen: boolean
|
||||||
|
focusMode: boolean
|
||||||
}) {
|
}) {
|
||||||
const { isOpen, setIsOpen, togglePane, setResizing } = useRailContext()
|
const { isOpen, setIsOpen, togglePane, setResizing } = useRailContext()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalResizeHandle
|
<HorizontalResizeHandle
|
||||||
className={classNames({ hidden: isReviewPanelOpen })}
|
className={classNames({ hidden: isReviewPanelOpen || focusMode })}
|
||||||
resizable
|
resizable
|
||||||
hitAreaMargins={{ coarse: 0, fine: 0 }}
|
hitAreaMargins={{ coarse: 0, fine: 0 }}
|
||||||
onDoubleClick={togglePane}
|
onDoubleClick={togglePane}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const RailLayout = () => {
|
|||||||
const gitBridgeEnabled = getMeta('ol-gitBridgeEnabled')
|
const gitBridgeEnabled = getMeta('ol-gitBridgeEnabled')
|
||||||
const { isOverleaf } = getMeta('ol-ExposedSettings')
|
const { isOverleaf } = getMeta('ol-ExposedSettings')
|
||||||
|
|
||||||
const { view, setLeftMenuShown } = useLayoutContext()
|
const { view, setLeftMenuShown, focusMode } = useLayoutContext()
|
||||||
|
|
||||||
const { markMessagesAsRead } = useChatContext()
|
const { markMessagesAsRead } = useChatContext()
|
||||||
|
|
||||||
@@ -271,7 +271,9 @@ export const RailLayout = () => {
|
|||||||
But it should be identified as a navigation landmark.
|
But it should be identified as a navigation landmark.
|
||||||
Therefore, we nest them: the parent <nav> is the landmark, and its child gets the "role="tablist"". */}
|
Therefore, we nest them: the parent <nav> is the landmark, and its child gets the "role="tablist"". */}
|
||||||
<nav
|
<nav
|
||||||
className={classNames('ide-rail', { hidden: isHistoryView })}
|
className={classNames('ide-rail', {
|
||||||
|
hidden: isHistoryView || focusMode,
|
||||||
|
})}
|
||||||
aria-label={t('sidebar')}
|
aria-label={t('sidebar')}
|
||||||
>
|
>
|
||||||
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
|
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
|
||||||
@@ -306,17 +308,20 @@ export const RailLayout = () => {
|
|||||||
</nav>
|
</nav>
|
||||||
</Nav>
|
</Nav>
|
||||||
</nav>
|
</nav>
|
||||||
{moduleRailPopovers
|
{!focusMode &&
|
||||||
.filter(shouldIncludeElement)
|
moduleRailPopovers
|
||||||
.map(({ key, Component, ref }) => (
|
.filter(shouldIncludeElement)
|
||||||
<Component key={key} ref={ref} />
|
.map(({ key, Component, ref }) => <Component key={key} ref={ref} />)}
|
||||||
))}
|
|
||||||
<RailPanel
|
<RailPanel
|
||||||
isReviewPanelOpen={isReviewPanelOpen}
|
isReviewPanelOpen={isReviewPanelOpen}
|
||||||
isHistoryView={isHistoryView}
|
isHistoryView={isHistoryView}
|
||||||
railTabs={railTabs}
|
railTabs={railTabs}
|
||||||
|
focusMode={focusMode}
|
||||||
|
/>
|
||||||
|
<RailResizeHandle
|
||||||
|
isReviewPanelOpen={isReviewPanelOpen}
|
||||||
|
focusMode={focusMode}
|
||||||
/>
|
/>
|
||||||
<RailResizeHandle isReviewPanelOpen={isReviewPanelOpen} />
|
|
||||||
<RailModals />
|
<RailModals />
|
||||||
</TabContainer>
|
</TabContainer>
|
||||||
)
|
)
|
||||||
|
|||||||
+40
-2
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownHeader,
|
DropdownHeader,
|
||||||
|
DropdownDivider,
|
||||||
} from '@/shared/components/dropdown/dropdown-menu'
|
} from '@/shared/components/dropdown/dropdown-menu'
|
||||||
import {
|
import {
|
||||||
IdeLayout,
|
IdeLayout,
|
||||||
@@ -14,8 +15,16 @@ import OLSpinner from '@/shared/components/ol/ol-spinner'
|
|||||||
import { isMac } from '@/shared/utils/os'
|
import { isMac } from '@/shared/utils/os'
|
||||||
import { Shortcut } from '@/shared/components/shortcut'
|
import { Shortcut } from '@/shared/components/shortcut'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||||
|
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||||
|
import SplitTestBadge from '@/shared/components/split-test-badge'
|
||||||
|
|
||||||
type LayoutOption = 'sideBySide' | 'editorOnly' | 'pdfOnly' | 'detachedPdf'
|
type LayoutOption =
|
||||||
|
| 'sideBySide'
|
||||||
|
| 'editorOnly'
|
||||||
|
| 'pdfOnly'
|
||||||
|
| 'detachedPdf'
|
||||||
|
| 'focusMode'
|
||||||
|
|
||||||
const getActiveLayoutOption = ({
|
const getActiveLayoutOption = ({
|
||||||
pdfLayout,
|
pdfLayout,
|
||||||
@@ -45,7 +54,6 @@ const getActiveLayoutOption = ({
|
|||||||
if (pdfLayout === 'sideBySide') {
|
if (pdfLayout === 'sideBySide') {
|
||||||
return 'sideBySide'
|
return 'sideBySide'
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,12 +101,14 @@ const shortcuts: Record<LayoutOption, string[] | null> = isMac
|
|||||||
pdfOnly: ['⌃', '⌘', '→'],
|
pdfOnly: ['⌃', '⌘', '→'],
|
||||||
sideBySide: ['⌃', '⌘', '↓'],
|
sideBySide: ['⌃', '⌘', '↓'],
|
||||||
detachedPdf: ['⌃', '⌘', '↑'],
|
detachedPdf: ['⌃', '⌘', '↑'],
|
||||||
|
focusMode: ['⌘', '⇧', 'M'],
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
editorOnly: null,
|
editorOnly: null,
|
||||||
pdfOnly: null,
|
pdfOnly: null,
|
||||||
sideBySide: null,
|
sideBySide: null,
|
||||||
detachedPdf: null,
|
detachedPdf: null,
|
||||||
|
focusMode: ['⌃', '⇧', 'M'],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChangeLayoutOptions() {
|
export default function ChangeLayoutOptions() {
|
||||||
@@ -109,9 +119,13 @@ export default function ChangeLayoutOptions() {
|
|||||||
pdfLayout,
|
pdfLayout,
|
||||||
handleChangeLayout,
|
handleChangeLayout,
|
||||||
handleDetach,
|
handleDetach,
|
||||||
|
focusMode,
|
||||||
|
setFocusMode,
|
||||||
} = useLayoutContext()
|
} = useLayoutContext()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { sendEvent } = useEditorAnalytics()
|
||||||
|
const focusModeEnabled = useFeatureFlag('focus-mode')
|
||||||
|
|
||||||
const detachable = 'BroadcastChannel' in window
|
const detachable = 'BroadcastChannel' in window
|
||||||
|
|
||||||
@@ -168,6 +182,30 @@ export default function ChangeLayoutOptions() {
|
|||||||
>
|
>
|
||||||
{t('open_pdf_in_separate_tab')}
|
{t('open_pdf_in_separate_tab')}
|
||||||
</LayoutDropdownItem>
|
</LayoutDropdownItem>
|
||||||
|
{focusModeEnabled && (
|
||||||
|
<>
|
||||||
|
<DropdownDivider />
|
||||||
|
<LayoutDropdownItem
|
||||||
|
onClick={() => {
|
||||||
|
setFocusMode(!focusMode)
|
||||||
|
sendEvent('focus-mode-toggle', { focusMode: !focusMode })
|
||||||
|
}}
|
||||||
|
active={focusMode}
|
||||||
|
leadingIcon="self_improvement"
|
||||||
|
trailingIcon={
|
||||||
|
<span className="d-flex align-items-center gap-2">
|
||||||
|
<SplitTestBadge
|
||||||
|
splitTestName="focus-mode"
|
||||||
|
displayOnVariants={['enabled']}
|
||||||
|
/>
|
||||||
|
{shortcuts.focusMode && <Shortcut keys={shortcuts.focusMode} />}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('focus_mode')}
|
||||||
|
</LayoutDropdownItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,16 @@ import UpgradeButton from './upgrade-button'
|
|||||||
import getMeta from '@/utils/meta'
|
import getMeta from '@/utils/meta'
|
||||||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||||
|
import OLIconButton from '@/shared/components/ol/ol-icon-button'
|
||||||
|
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||||
|
import SplitTestBadge from '@/shared/components/split-test-badge'
|
||||||
|
|
||||||
const [publishModalModules] = importOverleafModules('publishModal')
|
const [publishModalModules] = importOverleafModules('publishModal')
|
||||||
const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton
|
const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const { view, restoreView } = useLayoutContext()
|
const { view, restoreView, focusMode, setFocusMode, pdfLayout, setView } =
|
||||||
|
useLayoutContext()
|
||||||
const { cobranding, isRestrictedTokenMember } = useEditorContext()
|
const { cobranding, isRestrictedTokenMember } = useEditorContext()
|
||||||
const { permissionsLevel } = useIdeReactContext()
|
const { permissionsLevel } = useIdeReactContext()
|
||||||
const showUpgradePrompt = getMeta('ol-showUpgradePrompt')
|
const showUpgradePrompt = getMeta('ol-showUpgradePrompt')
|
||||||
@@ -38,6 +42,72 @@ export const Toolbar = () => {
|
|||||||
restoreView()
|
restoreView()
|
||||||
}, [restoreView])
|
}, [restoreView])
|
||||||
|
|
||||||
|
const handleExitFocusMode = useCallback(() => {
|
||||||
|
setFocusMode(false)
|
||||||
|
eventTracking.sendMB('focus-mode-exit')
|
||||||
|
}, [setFocusMode])
|
||||||
|
|
||||||
|
const handleSwitchView = useCallback(() => {
|
||||||
|
const newView = view === 'pdf' ? 'editor' : 'pdf'
|
||||||
|
setView(newView)
|
||||||
|
eventTracking.sendMB('focus-mode-switch-view', { view: newView })
|
||||||
|
}, [view, setView])
|
||||||
|
|
||||||
|
if (focusMode) {
|
||||||
|
const showViewSwitcher = pdfLayout === 'flat'
|
||||||
|
const switchTooltip =
|
||||||
|
view === 'pdf' ? t('switch_to_editor') : t('switch_to_pdf')
|
||||||
|
const switchIcon = view === 'pdf' ? 'edit' : 'picture_as_pdf'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="ide-redesign-toolbar" aria-label={t('project_actions')}>
|
||||||
|
<div className="ide-redesign-toolbar-menu">
|
||||||
|
<ToolbarLogos cobranding={cobranding} />
|
||||||
|
</div>
|
||||||
|
<ToolbarProjectTitle />
|
||||||
|
<div className="ide-redesign-toolbar-actions">
|
||||||
|
<div className="ide-redesign-toolbar-button-container">
|
||||||
|
<SplitTestBadge
|
||||||
|
splitTestName="focus-mode"
|
||||||
|
displayOnVariants={['enabled']}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showViewSwitcher && (
|
||||||
|
<div className="ide-redesign-toolbar-button-container">
|
||||||
|
<OLTooltip
|
||||||
|
id="tooltip-switch-view"
|
||||||
|
description={switchTooltip}
|
||||||
|
overlayProps={{ delay: 0, placement: 'bottom' }}
|
||||||
|
>
|
||||||
|
<OLIconButton
|
||||||
|
icon={switchIcon}
|
||||||
|
className="ide-redesign-toolbar-button-subdued ide-redesign-toolbar-button-icon"
|
||||||
|
onClick={handleSwitchView}
|
||||||
|
accessibilityLabel={switchTooltip}
|
||||||
|
/>
|
||||||
|
</OLTooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ChangeLayoutButton />
|
||||||
|
<div className="ide-redesign-toolbar-button-container">
|
||||||
|
<OLTooltip
|
||||||
|
id="tooltip-exit-focus-mode"
|
||||||
|
description={t('exit_focus_mode')}
|
||||||
|
overlayProps={{ delay: 0, placement: 'bottom' }}
|
||||||
|
>
|
||||||
|
<OLIconButton
|
||||||
|
icon="close"
|
||||||
|
className="ide-redesign-toolbar-button-subdued ide-redesign-toolbar-button-icon"
|
||||||
|
onClick={handleExitFocusMode}
|
||||||
|
accessibilityLabel={t('exit_focus_mode')}
|
||||||
|
/>
|
||||||
|
</OLTooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (view === 'history') {
|
if (view === 'history') {
|
||||||
return (
|
return (
|
||||||
<nav className="ide-redesign-toolbar" aria-label={t('project_actions')}>
|
<nav className="ide-redesign-toolbar" aria-label={t('project_actions')}>
|
||||||
|
|||||||
+3
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import OLButtonToolbar from '@/shared/components/ol/ol-button-toolbar'
|
import OLButtonToolbar from '@/shared/components/ol/ol-button-toolbar'
|
||||||
@@ -34,6 +35,7 @@ function PdfPreviewHybridToolbar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PdfPreviewHybridToolbarInner() {
|
function PdfPreviewHybridToolbarInner() {
|
||||||
|
const { focusMode } = useLayoutContext()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="toolbar-pdf-left">
|
<div className="toolbar-pdf-left">
|
||||||
@@ -43,7 +45,7 @@ function PdfPreviewHybridToolbarInner() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="toolbar-pdf-right">
|
<div className="toolbar-pdf-right">
|
||||||
<div className="toolbar-pdf-controls" id="toolbar-pdf-controls" />
|
<div className="toolbar-pdf-controls" id="toolbar-pdf-controls" />
|
||||||
<SwitchToEditorButton />
|
{!focusMode && <SwitchToEditorButton />}
|
||||||
<DetachedSynctexControl />
|
<DetachedSynctexControl />
|
||||||
{/* TODO: should we have code check? */}
|
{/* TODO: should we have code check? */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-1
@@ -4,10 +4,12 @@ import { memo } from 'react'
|
|||||||
import ReviewPanel from './review-panel'
|
import ReviewPanel from './review-panel'
|
||||||
import ReviewModeSwitcher from './review-mode-switcher'
|
import ReviewModeSwitcher from './review-mode-switcher'
|
||||||
import useReviewPanelLayout from '../hooks/use-review-panel-layout'
|
import useReviewPanelLayout from '../hooks/use-review-panel-layout'
|
||||||
|
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||||
|
|
||||||
function ReviewPanelContainer() {
|
function ReviewPanelContainer() {
|
||||||
const view = useCodeMirrorViewContext()
|
const view = useCodeMirrorViewContext()
|
||||||
const { showPanel, mini } = useReviewPanelLayout()
|
const { showPanel, mini } = useReviewPanelLayout()
|
||||||
|
const { focusMode } = useLayoutContext()
|
||||||
|
|
||||||
if (!view) {
|
if (!view) {
|
||||||
return null
|
return null
|
||||||
@@ -15,7 +17,7 @@ function ReviewPanelContainer() {
|
|||||||
|
|
||||||
return ReactDOM.createPortal(
|
return ReactDOM.createPortal(
|
||||||
<>
|
<>
|
||||||
<ReviewModeSwitcher />
|
{!focusMode && <ReviewModeSwitcher />}
|
||||||
{showPanel && <ReviewPanel mini={mini} />}
|
{showPanel && <ReviewPanel mini={mini} />}
|
||||||
</>,
|
</>,
|
||||||
view.scrollDOM
|
view.scrollDOM
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRangesContext } from '../context/ranges-context'
|
import { useRangesContext } from '../context/ranges-context'
|
||||||
import { useThreadsContext } from '@/features/review-panel/context/threads-context'
|
import { useThreadsContext } from '@/features/review-panel/context/threads-context'
|
||||||
import { hasActiveRange } from '@/features/review-panel/utils/has-active-range'
|
import { hasActiveRange } from '@/features/review-panel/utils/has-active-range'
|
||||||
|
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||||
import { useRailContext } from '@/features/ide-react/context/rail-context'
|
import { useRailContext } from '@/features/ide-react/context/rail-context'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ export default function useReviewPanelLayout(): {
|
|||||||
openTab: openRailTab,
|
openTab: openRailTab,
|
||||||
setIsOpen: setRailIsOpen,
|
setIsOpen: setRailIsOpen,
|
||||||
} = useRailContext()
|
} = useRailContext()
|
||||||
|
const { focusMode } = useLayoutContext()
|
||||||
|
|
||||||
const reviewPanelOpen = selectedRailTab === 'review-panel' && railIsOpen
|
const reviewPanelOpen = selectedRailTab === 'review-panel' && railIsOpen
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ export default function useReviewPanelLayout(): {
|
|||||||
}, [setRailIsOpen])
|
}, [setRailIsOpen])
|
||||||
|
|
||||||
const hasCommentOrChange = hasActiveRange(ranges, threads)
|
const hasCommentOrChange = hasActiveRange(ranges, threads)
|
||||||
const showPanel = reviewPanelOpen || !!hasCommentOrChange
|
const showPanel = focusMode ? false : reviewPanelOpen || !!hasCommentOrChange
|
||||||
const mini = !reviewPanelOpen
|
const mini = !reviewPanelOpen
|
||||||
const showHeader = showPanel && !mini
|
const showHeader = showPanel && !mini
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import classNames from 'classnames'
|
|||||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||||
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
|
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
|
||||||
|
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||||
|
|
||||||
const sourceEditorToolbarComponents = importOverleafModules(
|
const sourceEditorToolbarComponents = importOverleafModules(
|
||||||
'sourceEditorToolbarComponents'
|
'sourceEditorToolbarComponents'
|
||||||
@@ -62,6 +63,7 @@ const Toolbar = memo(function Toolbar() {
|
|||||||
userSettings: { breadcrumbs },
|
userSettings: { breadcrumbs },
|
||||||
} = useUserSettingsContext()
|
} = useUserSettingsContext()
|
||||||
const visualPreviewEnabled = useFeatureFlag('visual-preview')
|
const visualPreviewEnabled = useFeatureFlag('visual-preview')
|
||||||
|
const { focusMode } = useLayoutContext()
|
||||||
|
|
||||||
const [overflowed, setOverflowed] = useState(false)
|
const [overflowed, setOverflowed] = useState(false)
|
||||||
|
|
||||||
@@ -158,6 +160,10 @@ const Toolbar = memo(function Toolbar() {
|
|||||||
|
|
||||||
const showActions = !state.readOnly && !insideTable
|
const showActions = !state.readOnly && !insideTable
|
||||||
|
|
||||||
|
if (focusMode) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showReviewPanelHeader && <ReviewPanelHeader />}
|
{showReviewPanelHeader && <ReviewPanelHeader />}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { useRailContext } from '@/features/ide-react/context/rail-context'
|
|||||||
import usePersistedState from '@/shared/hooks/use-persisted-state'
|
import usePersistedState from '@/shared/hooks/use-persisted-state'
|
||||||
import { repositionAllTooltips } from '@/features/source-editor/extensions/tooltips-reposition'
|
import { repositionAllTooltips } from '@/features/source-editor/extensions/tooltips-reposition'
|
||||||
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
|
||||||
|
import { useFeatureFlag } from './split-test-context'
|
||||||
|
|
||||||
export type IdeLayout = 'sideBySide' | 'flat'
|
export type IdeLayout = 'sideBySide' | 'flat'
|
||||||
export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
|
export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
|
||||||
@@ -38,6 +39,7 @@ export type LayoutContextOwnStates = {
|
|||||||
pdfLayout: IdeLayout
|
pdfLayout: IdeLayout
|
||||||
projectSearchIsOpen: boolean
|
projectSearchIsOpen: boolean
|
||||||
openFile: BinaryFile | null
|
openFile: BinaryFile | null
|
||||||
|
focusMode: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LayoutContextValue = LayoutContextOwnStates & {
|
export type LayoutContextValue = LayoutContextOwnStates & {
|
||||||
@@ -66,6 +68,7 @@ export type LayoutContextValue = LayoutContextOwnStates & {
|
|||||||
restoreView: () => void
|
restoreView: () => void
|
||||||
handleChangeLayout: (newLayout: IdeLayout, newView?: IdeView) => void
|
handleChangeLayout: (newLayout: IdeLayout, newView?: IdeView) => void
|
||||||
handleDetach: () => void
|
handleDetach: () => void
|
||||||
|
setFocusMode: Dispatch<SetStateAction<LayoutContextValue['focusMode']>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const debugPdfDetach = getMeta('ol-debugPdfDetach')
|
const debugPdfDetach = getMeta('ol-debugPdfDetach')
|
||||||
@@ -154,6 +157,37 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
|||||||
// whether the project search is open
|
// whether the project search is open
|
||||||
const [projectSearchIsOpen, setProjectSearchIsOpen] = useState(false)
|
const [projectSearchIsOpen, setProjectSearchIsOpen] = useState(false)
|
||||||
|
|
||||||
|
// whether to display the editor and preview side-by-side or full-width ("flat")
|
||||||
|
const [pdfLayout, setPdfLayout] = useState<IdeLayout>('sideBySide')
|
||||||
|
|
||||||
|
const [persistedFocusMode, setPersistedFocusMode] =
|
||||||
|
usePersistedState<boolean>('ui.focus-mode', false)
|
||||||
|
|
||||||
|
const focusModeEnabled = useFeatureFlag('focus-mode')
|
||||||
|
|
||||||
|
const focusMode = focusModeEnabled && persistedFocusMode
|
||||||
|
const setFocusMode = useCallback(
|
||||||
|
(value: SetStateAction<boolean>) => {
|
||||||
|
if (focusModeEnabled) {
|
||||||
|
const newValue = typeof value === 'function' ? value(focusMode) : value
|
||||||
|
setPersistedFocusMode(newValue)
|
||||||
|
sendEvent('project-layout-change', {
|
||||||
|
layout: pdfLayout,
|
||||||
|
view: view ?? undefined,
|
||||||
|
focusMode: newValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
focusMode,
|
||||||
|
focusModeEnabled,
|
||||||
|
pdfLayout,
|
||||||
|
sendEvent,
|
||||||
|
setPersistedFocusMode,
|
||||||
|
view,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
useEventListener(
|
useEventListener(
|
||||||
'ui.toggle-left-menu',
|
'ui.toggle-left-menu',
|
||||||
useCallback(
|
useCallback(
|
||||||
@@ -172,14 +206,21 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
|||||||
}, [setReviewPanelOpen])
|
}, [setReviewPanelOpen])
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO ide-redesign-cleanup: remove this listener as we have an equivalent in rail-context
|
|
||||||
useEventListener(
|
useEventListener(
|
||||||
'keydown',
|
'ui.toggle-focus-mode',
|
||||||
useCallback((event: KeyboardEvent) => {
|
useCallback(() => {
|
||||||
|
setFocusMode(mode => !mode)
|
||||||
|
}, [setFocusMode])
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global keyboard shortcut handlers
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
// Cmd/Ctrl+Shift+F for full project search
|
||||||
if (
|
if (
|
||||||
(isMac ? event.metaKey : event.ctrlKey) &&
|
(isMac ? event.metaKey : event.ctrlKey) &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
event.code === 'KeyF'
|
event.key === 'F'
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
sendSearchEvent('search-open', {
|
sendSearchEvent('search-open', {
|
||||||
@@ -188,11 +229,25 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
|||||||
})
|
})
|
||||||
setProjectSearchIsOpen(true)
|
setProjectSearchIsOpen(true)
|
||||||
}
|
}
|
||||||
}, [])
|
// Cmd/Ctrl+Shift+M for focus mode
|
||||||
)
|
if (
|
||||||
|
focusModeEnabled &&
|
||||||
|
(isMac ? event.metaKey : event.ctrlKey) &&
|
||||||
|
event.shiftKey &&
|
||||||
|
event.key === 'M'
|
||||||
|
) {
|
||||||
|
event.preventDefault()
|
||||||
|
setFocusMode(mode => !mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// whether to display the editor and preview side-by-side or full-width ("flat")
|
// Use capture phase to ensure we get the event even if something else stops propagation
|
||||||
const [pdfLayout, setPdfLayout] = useState<IdeLayout>('sideBySide')
|
window.addEventListener('keydown', handleKeyDown, true)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown, true)
|
||||||
|
}
|
||||||
|
}, [focusModeEnabled, setFocusMode, setProjectSearchIsOpen])
|
||||||
|
|
||||||
// whether stylesheet on theme is loading
|
// whether stylesheet on theme is loading
|
||||||
const [loadingStyleSheet, setLoadingStyleSheet] = useState(false)
|
const [loadingStyleSheet, setLoadingStyleSheet] = useState(false)
|
||||||
@@ -282,9 +337,10 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
|||||||
sendEvent('project-layout-change', {
|
sendEvent('project-layout-change', {
|
||||||
layout: newLayout,
|
layout: newLayout,
|
||||||
view: newView,
|
view: newView,
|
||||||
|
focusMode,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[changeLayout, handleReattach, sendEvent]
|
[changeLayout, focusMode, handleReattach, sendEvent]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEventListener(
|
useEventListener(
|
||||||
@@ -353,6 +409,8 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
|||||||
restoreView,
|
restoreView,
|
||||||
handleChangeLayout,
|
handleChangeLayout,
|
||||||
handleDetach,
|
handleDetach,
|
||||||
|
focusMode,
|
||||||
|
setFocusMode,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
reattach,
|
reattach,
|
||||||
@@ -382,6 +440,8 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
|||||||
restoreView,
|
restoreView,
|
||||||
handleChangeLayout,
|
handleChangeLayout,
|
||||||
handleDetach,
|
handleDetach,
|
||||||
|
focusMode,
|
||||||
|
setFocusMode,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -849,6 +849,7 @@
|
|||||||
"examples_to_help_you_learn": "Examples to help you learn how to use powerful LaTeX packages and techniques.",
|
"examples_to_help_you_learn": "Examples to help you learn how to use powerful LaTeX packages and techniques.",
|
||||||
"exclusive_access_with_labs": "Exclusive access to early-stage experiments",
|
"exclusive_access_with_labs": "Exclusive access to early-stage experiments",
|
||||||
"existing_plan_active_until_term_end": "Your existing plan and its features will remain active until the end of the current billing period.",
|
"existing_plan_active_until_term_end": "Your existing plan and its features will remain active until the end of the current billing period.",
|
||||||
|
"exit_focus_mode": "Exit focus mode",
|
||||||
"expand": "Expand",
|
"expand": "Expand",
|
||||||
"experiment_enabled_refresh_page": "Experiment is enabled! You need to refresh the page to see the changes.",
|
"experiment_enabled_refresh_page": "Experiment is enabled! You need to refresh the page to see the changes.",
|
||||||
"experiment_full_check_back_soon": "Sorry, this experiment is full. Spaces may become available, so check back soon.",
|
"experiment_full_check_back_soon": "Sorry, this experiment is full. Spaces may become available, so check back soon.",
|
||||||
@@ -927,6 +928,7 @@
|
|||||||
"fixed_width_wrap_text": "Fixed width, wrap text",
|
"fixed_width_wrap_text": "Fixed width, wrap text",
|
||||||
"flexible_plans_for_everyone": "Flexible plans for everyone—from individual students and researchers, to large businesses and universities.",
|
"flexible_plans_for_everyone": "Flexible plans for everyone—from individual students and researchers, to large businesses and universities.",
|
||||||
"flexible_plans_for_everyone_from_individual_students_and_researchers_to_large_businesses_and_universities": "Flexible plans for everyone, from individual students and researchers to large businesses and universities.",
|
"flexible_plans_for_everyone_from_individual_students_and_researchers_to_large_businesses_and_universities": "Flexible plans for everyone, from individual students and researchers to large businesses and universities.",
|
||||||
|
"focus_mode": "Focus mode",
|
||||||
"fold_line": "Fold line",
|
"fold_line": "Fold line",
|
||||||
"folder_location": "Folder location",
|
"folder_location": "Folder location",
|
||||||
"folder_name": "Folder name",
|
"folder_name": "Folder name",
|
||||||
|
|||||||
+2
@@ -98,6 +98,8 @@ const createInitialValue = () =>
|
|||||||
restoreView: cy.stub(),
|
restoreView: cy.stub(),
|
||||||
handleChangeLayout: cy.stub(),
|
handleChangeLayout: cy.stub(),
|
||||||
handleDetach: cy.stub(),
|
handleDetach: cy.stub(),
|
||||||
|
focusMode: false,
|
||||||
|
setFocusMode: cy.stub(),
|
||||||
}) satisfies LayoutContextValue
|
}) satisfies LayoutContextValue
|
||||||
|
|
||||||
const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||||
|
|||||||
@@ -577,6 +577,8 @@ const makeLayoutProvider = (
|
|||||||
restoreView,
|
restoreView,
|
||||||
handleChangeLayout,
|
handleChangeLayout,
|
||||||
handleDetach,
|
handleDetach,
|
||||||
|
focusMode: layout.focusMode ?? false,
|
||||||
|
setFocusMode: layout.setFocusMode ?? (() => {}),
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
reattach,
|
reattach,
|
||||||
|
|||||||
Reference in New Issue
Block a user