diff --git a/services/web/frontend/js/features/review-panel/components/review-mode-switcher.tsx b/services/web/frontend/js/features/review-panel/components/review-mode-switcher.tsx
index 31ae4c74cb..7f377edb09 100644
--- a/services/web/frontend/js/features/review-panel/components/review-mode-switcher.tsx
+++ b/services/web/frontend/js/features/review-panel/components/review-mode-switcher.tsx
@@ -18,8 +18,9 @@ import { useProjectContext } from '@/shared/context/project-context'
import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-context'
import { useEditorContext } from '@/shared/context/editor-context'
import { useTrackingChangesMode } from '@/shared/hooks/use-tracking-changes-mode'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
-function ReviewModeSwitcher() {
+function ReviewModeDropdownItems() {
const { t } = useTranslation()
const user = useUserContext()
const { saveTrackChangesForCurrentUser, saveTrackChanges } =
@@ -32,6 +33,108 @@ function ReviewModeSwitcher() {
const showViewOption = permissionsLevel === 'readOnly'
const view = useCodeMirrorViewContext()
+ return (
+ <>
+ {
+ if (mode === 'edit') {
+ view.focus()
+ return
+ }
+ sendMB('editing-mode-change', {
+ role: permissionsLevel,
+ previousMode: mode,
+ newMode: 'edit',
+ })
+ if (user?.id) {
+ saveTrackChangesForCurrentUser(false)
+ } else {
+ saveTrackChanges({ on_for_guests: false })
+ }
+ view.focus()
+ }}
+ description={t('edit_content_directly')}
+ leadingIcon="edit"
+ active={write && mode === 'edit'}
+ >
+ {t('editing')}
+
+ {
+ if (mode === 'review') {
+ view.focus()
+ return
+ }
+ if (!features.trackChanges) {
+ setUpgradeTrackChangesModal({
+ show: true,
+ location: 'review-switcher',
+ })
+ } else {
+ sendMB('editing-mode-change', {
+ role: permissionsLevel,
+ previousMode: mode,
+ newMode: 'review',
+ })
+ if (user?.id) {
+ saveTrackChangesForCurrentUser(true)
+ } else {
+ saveTrackChanges({ on_for_guests: true })
+ }
+ view.focus()
+ }
+ }}
+ description={
+ permissionsLevel === 'review' && !trackedWrite
+ ? t('comment_only')
+ : t('edits_become_suggestions')
+ }
+ leadingIcon="rate_review"
+ active={trackedWrite && mode === 'review'}
+ >
+ {t('reviewing')}
+
+ {showViewOption && (
+
+ {t('viewing')}
+
+ )}
+ >
+ )
+}
+
+function ReviewModeSwitcher() {
+ const isToolbarMigration = useFeatureFlag('writefull-toolbar-migration')
+
+ if (isToolbarMigration) {
+ return (
+
+
+
+
+
+
+ )
+ }
+
+ // Portal variant (default): sticky container keeps the button floating
+ // over the editor at top-right of the scroll area.
return (
@@ -40,76 +143,7 @@ function ReviewModeSwitcher() {
id="review-mode-switcher"
/>
- {
- if (mode === 'edit') {
- view.focus()
- return
- }
- sendMB('editing-mode-change', {
- role: permissionsLevel,
- previousMode: mode,
- newMode: 'edit',
- })
- if (user?.id) {
- saveTrackChangesForCurrentUser(false)
- } else {
- saveTrackChanges({ on_for_guests: false })
- }
- view.focus()
- }}
- description={t('edit_content_directly')}
- leadingIcon="edit"
- active={write && mode === 'edit'}
- >
- {t('editing')}
-
- {
- if (mode === 'review') {
- view.focus()
- return
- }
- if (!features.trackChanges) {
- setUpgradeTrackChangesModal({
- show: true,
- location: 'review-switcher',
- })
- } else {
- sendMB('editing-mode-change', {
- role: permissionsLevel,
- previousMode: mode,
- newMode: 'review',
- })
- if (user?.id) {
- saveTrackChangesForCurrentUser(true)
- } else {
- saveTrackChanges({ on_for_guests: true })
- }
- view.focus()
- }
- }}
- description={
- permissionsLevel === 'review' && !trackedWrite
- ? t('comment_only')
- : t('edits_become_suggestions')
- }
- leadingIcon="rate_review"
- active={trackedWrite && mode === 'review'}
- >
- {t('reviewing')}
-
- {showViewOption && (
-
- {t('viewing')}
-
- )}
+
diff --git a/services/web/frontend/js/features/review-panel/components/review-panel-container.tsx b/services/web/frontend/js/features/review-panel/components/review-panel-container.tsx
index 4108a6ee0f..e5201e842e 100644
--- a/services/web/frontend/js/features/review-panel/components/review-panel-container.tsx
+++ b/services/web/frontend/js/features/review-panel/components/review-panel-container.tsx
@@ -5,11 +5,13 @@ import ReviewPanel from './review-panel'
import ReviewModeSwitcher from './review-mode-switcher'
import useReviewPanelLayout from '../hooks/use-review-panel-layout'
import { useLayoutContext } from '@/shared/context/layout-context'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
function ReviewPanelContainer() {
const view = useCodeMirrorViewContext()
const { showPanel, mini } = useReviewPanelLayout()
const { focusMode } = useLayoutContext()
+ const isToolbarMigration = useFeatureFlag('writefull-toolbar-migration')
if (!view) {
return null
@@ -17,7 +19,7 @@ function ReviewPanelContainer() {
return ReactDOM.createPortal(
<>
- {!focusMode && }
+ {!focusMode && !isToolbarMigration && }
{showPanel && }
>,
view.scrollDOM
diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx
index d7f3256dec..fcde83c988 100644
--- a/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx
+++ b/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx
@@ -18,6 +18,7 @@ import useDropdown from '../../../shared/hooks/use-dropdown'
import { getPanel } from '@codemirror/view'
import { createToolbarPanel } from '../extensions/toolbar/toolbar-panel'
import EditorSwitch from './editor-switch'
+import ReviewModeSwitcher from '@/features/review-panel/components/review-mode-switcher'
import SwitchToPDFButton from './switch-to-pdf-button'
import { DetacherSynctexControl } from '../../pdf-preview/components/detach-synctex-control'
import DetachCompileButtonWrapper from '../../pdf-preview/components/detach-compile-button-wrapper'
@@ -33,6 +34,7 @@ import Breadcrumbs from '@/features/source-editor/extensions/breadcrumbs'
import classNames from 'classnames'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { useFeatureFlag } from '@/shared/context/split-test-context'
+import { useProjectContext } from '@/shared/context/project-context'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { useLayoutContext } from '@/shared/context/layout-context'
import ReviewPanelHeaderBuffer from '@/features/review-panel/components/review-panel-header-buffer'
@@ -69,6 +71,8 @@ const Toolbar = memo(function Toolbar() {
userSettings: { breadcrumbs },
} = useUserSettingsContext()
const visualPreviewEnabled = useFeatureFlag('visual-preview')
+ const isToolbarMigration = useFeatureFlag('writefull-toolbar-migration')
+ const { features } = useProjectContext()
const { focusMode } = useLayoutContext()
const [overflowed, setOverflowed] = useState(false)
@@ -234,6 +238,13 @@ const Toolbar = memo(function Toolbar() {
{!visualPreviewEnabled && }
+ {/* trackChangesVisible controls provider/UI availability; trackChanges
+ (checked inside the switcher) controls the actual feature entitlement.
+ Users with trackChangesVisible:true but trackChanges:false see the
+ switcher and get an upgrade modal when clicking "Reviewing". */}
+ {isToolbarMigration && features.trackChangesVisible && (
+
+ )}
{sourceEditorToolbarEndButtons.map(
({ import: { default: Component }, path }) => (
diff --git a/services/web/frontend/stylesheets/pages/editor/review-panel.scss b/services/web/frontend/stylesheets/pages/editor/review-panel.scss
index 9d24f6a07e..c6d4068b35 100644
--- a/services/web/frontend/stylesheets/pages/editor/review-panel.scss
+++ b/services/web/frontend/stylesheets/pages/editor/review-panel.scss
@@ -11,6 +11,27 @@ $rp-type-blue: #6b7797;
--review-panel-width: 230px;
--review-panel-width-mini: 24px;
--review-panel-color-scheme: dark;
+ --review-mode-switcher-reviewing-bg: var(--yellow-10);
+ --review-mode-switcher-reviewing-hover-bg: var(--yellow-20);
+ --review-mode-switcher-reviewing-color: var(--yellow-60);
+
+ @include theme('default') {
+ .ide-redesign-main {
+ --review-panel-bg-color: var(--bg-dark-secondary);
+ --review-panel-color: var(--content-primary-dark);
+ --review-panel-color-secondary: var(--content-secondary-dark);
+ --review-panel-header-bg-color: var(--bg-dark-primary);
+ --review-panel-footer-bg-color: var(--bg-dark-primary);
+ --review-panel-entry-bg-color: var(--bg-dark-primary);
+ --review-panel-empty-state-bg-color: var(--bg-dark-primary);
+ --review-panel-button-hover-bg-color: var(--bg-dark-tertiary);
+ --review-panel-border-color: var(--border-divider-dark);
+ --review-panel-color-scheme: dark;
+ --review-mode-switcher-reviewing-bg: var(--yellow-70);
+ --review-mode-switcher-reviewing-hover-bg: var(--yellow-60);
+ --review-mode-switcher-reviewing-color: var(--yellow-10);
+ }
+ }
}
@include theme('light') {
@@ -752,3 +773,53 @@ del.review-panel-content-highlight {
}
}
}
+
+.ol-cm-toolbar {
+ .review-mode-switcher {
+ // In the toolbar the component is in normal flex flow — no absolute positioning.
+ position: static;
+
+ &:hover,
+ &:focus-within {
+ .review-mode-switcher-toggle-button.editing {
+ background-color: var(--review-panel-button-hover-bg-color);
+ }
+
+ .review-mode-switcher-toggle-button.reviewing {
+ background-color: var(--review-mode-switcher-reviewing-hover-bg);
+ }
+
+ .review-mode-switcher-toggle-button.viewing {
+ background-color: var(--blue-20);
+ }
+
+ // In the toolbar the label text does not expand on hover (would shift other buttons)
+ .review-mode-switcher-toggle-label {
+ display: none;
+ }
+ }
+ }
+
+ .review-mode-switcher-toggle-button {
+ border-radius: var(--border-radius-base);
+ padding: var(--spacing-01) var(--spacing-02);
+ gap: var(--spacing-01);
+ height: auto;
+ cursor: pointer;
+
+ &:focus-visible {
+ outline: 2px solid var(--content-primary);
+ outline-offset: 2px;
+ }
+
+ &.editing {
+ background-color: var(--toolbar-alt-bg-color);
+ color: var(--toolbar-btn-color);
+ }
+
+ &.reviewing {
+ background-color: var(--review-mode-switcher-reviewing-bg);
+ color: var(--review-mode-switcher-reviewing-color);
+ }
+ }
+}
diff --git a/services/web/frontend/stylesheets/pages/editor/toolbar.scss b/services/web/frontend/stylesheets/pages/editor/toolbar.scss
index 780de74d4d..0743ff79ee 100644
--- a/services/web/frontend/stylesheets/pages/editor/toolbar.scss
+++ b/services/web/frontend/stylesheets/pages/editor/toolbar.scss
@@ -295,6 +295,12 @@
z-index: 10; // Prevent track changes showing over toolbar
}
+// Isolate secondary toolbar rows so they paint below the main toolbar (z-index: 10)
+// and its dropdowns, which would otherwise be obscured due to DOM order.
+.ol-cm-toolbar-wrapper > .writefull {
+ isolation: isolate;
+}
+
/**************************************
Toggle Switch
***************************************/