diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 5d3bbf00df..df68da6ec1 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -441,6 +441,7 @@
"editor_disconected_click_to_reconnect": "",
"editor_limit_exceeded_in_this_project": "",
"editor_only_hide_pdf": "",
+ "editor_settings": "",
"editor_theme": "",
"educational_disclaimer": "",
"educational_disclaimer_heading": "",
@@ -554,6 +555,8 @@
"full_project_search": "",
"full_width": "",
"future_payments": "",
+ "general": "",
+ "general_settings": "",
"generate_token": "",
"generic_if_problem_continues_contact_us": "",
"generic_linked_file_compile_error": "",
@@ -757,6 +760,8 @@
"institutional_leavers_survey_notification": "",
"integrations": "",
"interested_in_cheaper_personal_plan": "",
+ "interface": "",
+ "interface_settings": "",
"invalid_confirmation_code": "",
"invalid_email": "",
"invalid_file_name": "",
@@ -1083,6 +1088,7 @@
"pay_now": "",
"payment_provider_unreachable_error": "",
"payment_summary": "",
+ "pdf": "",
"pdf_compile_in_progress_error": "",
"pdf_compile_rate_limit_hit": "",
"pdf_compile_try_again": "",
@@ -1090,6 +1096,7 @@
"pdf_only_hide_editor": "",
"pdf_preview_error": "",
"pdf_rendering_error": "",
+ "pdf_settings": "",
"pdf_unavailable_for_download": "",
"pdf_viewer": "",
"pdf_viewer_error": "",
diff --git a/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 b/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2
index 6eba080dd6..68ddbc999d 100644
Binary files a/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 and b/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 differ
diff --git a/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs b/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs
index 7463addc64..54052d8773 100644
--- a/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs
+++ b/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs
@@ -3,9 +3,14 @@
// to update the font file with the latest icons.
export default /** @type {const} */ ([
+ 'code',
'description',
'forum',
+ 'help',
'integration_instructions',
+ 'picture_as_pdf',
'rate_review',
'report',
+ 'settings',
+ 'web_asset',
])
diff --git a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx
index c73c5fe1ef..84f8ba87b2 100644
--- a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx
+++ b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx
@@ -16,6 +16,9 @@ import { useFeatureFlag } from '@/shared/context/split-test-context'
const MainLayoutNew = lazy(
() => import('@/features/ide-redesign/components/main-layout')
)
+const SettingsModalNew = lazy(
+ () => import('@/features/ide-redesign/components/settings/settings-modal')
+)
export default function IdePage() {
useLayoutEventTracking() // sent event when the layout changes
@@ -33,6 +36,7 @@ export default function IdePage() {
{newEditor ? (
+
) : (
diff --git a/services/web/frontend/js/features/ide-redesign/components/rail.tsx b/services/web/frontend/js/features/ide-redesign/components/rail.tsx
index a588dfa40d..e9d21af972 100644
--- a/services/web/frontend/js/features/ide-redesign/components/rail.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/rail.tsx
@@ -1,9 +1,10 @@
-import { ReactElement, useCallback, useState } from 'react'
+import { ReactElement, useCallback, useMemo, useState } from 'react'
import { Nav, NavLink, Tab, TabContainer } from 'react-bootstrap-5'
import MaterialIcon, {
AvailableUnfilledIcon,
} from '@/shared/components/material-icon'
import { Panel } from 'react-resizable-panels'
+import { useLayoutContext } from '@/shared/context/layout-context'
type RailElement = {
icon: AvailableUnfilledIcon
@@ -11,6 +12,14 @@ type RailElement = {
component: ReactElement
}
+type RailActionLink = { key: string; icon: AvailableUnfilledIcon; href: string }
+type RailActionButton = {
+ key: string
+ icon: AvailableUnfilledIcon
+ action: () => void
+}
+type RailAction = RailActionLink | RailActionButton
+
const RAIL_TABS: RailElement[] = [
// NOTE: The file tree **MUST** be the first (i.e. default) tab in the list
// since the file tree is responsible for opening the initial document.
@@ -45,6 +54,19 @@ export const RailLayout = () => {
const [selectedTab, setSelectedTab] = useState(
RAIL_TABS[0]?.key
)
+ const { setLeftMenuShown } = useLayoutContext()
+ const railActions: RailAction[] = useMemo(
+ () => [
+ { key: 'support', icon: 'help', href: '/learn' },
+ {
+ key: 'settings',
+ icon: 'settings',
+ action: () => setLeftMenuShown(true),
+ },
+ ],
+ [setLeftMenuShown]
+ )
+
return (
{
id="ide-rail-tabs"
>
-
)
}
+
+const RailActionElement = ({ action }: { action: RailAction }) => {
+ const icon = (
+
+ )
+ const onActionClick = useCallback(() => {
+ if ('action' in action) {
+ action.action()
+ }
+ }, [action])
+
+ if ('href' in action) {
+ return (
+
+ {icon}
+
+ )
+ } else {
+ return (
+
+ )
+ }
+}
diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal-body.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal-body.tsx
new file mode 100644
index 0000000000..c5143a2932
--- /dev/null
+++ b/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal-body.tsx
@@ -0,0 +1,147 @@
+import MaterialIcon, {
+ AvailableUnfilledIcon,
+} from '@/shared/components/material-icon'
+import { ReactElement, useMemo, useState } from 'react'
+import {
+ Nav,
+ NavLink,
+ TabContainer,
+ TabContent,
+ TabPane,
+} from 'react-bootstrap-5'
+import { useTranslation } from 'react-i18next'
+
+export type SettingsEntry = SettingsLink | SettingsTab
+
+type SettingsTab = {
+ icon: AvailableUnfilledIcon
+ key: string
+ component: ReactElement
+ title: string
+ subtitle: string
+}
+
+type SettingsLink = {
+ key: string
+ icon: AvailableUnfilledIcon
+ href: string
+ title: string
+}
+
+export const SettingsModalBody = () => {
+ const { t } = useTranslation()
+ const settingsTabs: SettingsEntry[] = useMemo(
+ () => [
+ {
+ key: 'general',
+ title: t('general'),
+ subtitle: t('general_settings'),
+ icon: 'settings',
+ component: General
,
+ },
+ {
+ key: 'editor',
+ title: t('editor'),
+ subtitle: t('editor_settings'),
+ icon: 'code',
+ component: Editor
,
+ },
+ {
+ key: 'pdf',
+ title: t('pdf'),
+ subtitle: t('pdf_settings'),
+ icon: 'picture_as_pdf',
+ component: PDF
,
+ },
+ {
+ key: 'interface',
+ title: t('interface'),
+ subtitle: t('interface_settings'),
+ icon: 'web_asset',
+ component: Interface
,
+ },
+ {
+ key: 'account_settings',
+ title: t('account_settings'),
+ icon: 'settings',
+ href: '/user/settings',
+ },
+ ],
+ [t]
+ )
+ const [activeTab, setActiveTab] = useState(
+ settingsTabs[0]?.key
+ )
+
+ return (
+
+
+
+ {settingsTabs.map(entry => (
+
+ ))}
+
+
+ {settingsTabs
+ .filter(t => 'component' in t)
+ .map(({ key, component, subtitle }) => (
+
+ {subtitle}
+ {component}
+
+ ))}
+
+
+
+ )
+}
+
+const SettingsNavLink = ({ entry }: { entry: SettingsEntry }) => {
+ if ('href' in entry) {
+ return (
+
+
+ {entry.title}
+
+
+
+ )
+ } else {
+ return (
+ <>
+
+
+ {entry.title}
+
+ >
+ )
+ }
+}
diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal.tsx
new file mode 100644
index 0000000000..8ee804159f
--- /dev/null
+++ b/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal.tsx
@@ -0,0 +1,31 @@
+import OLModal, {
+ OLModalBody,
+ OLModalHeader,
+ OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import { useLayoutContext } from '@/shared/context/layout-context'
+import { useTranslation } from 'react-i18next'
+import { SettingsModalBody } from './settings-modal-body'
+
+const SettingsModal = () => {
+ // TODO ide-redesign-cleanup: Either rename the field, or introduce a separate
+ // one
+ const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
+ const { t } = useTranslation()
+ return (
+ setLeftMenuShown(false)}
+ size="lg"
+ >
+
+ {t('settings')}
+
+
+
+
+
+ )
+}
+
+export default SettingsModal
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
index 956f28777c..454f0708c5 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
@@ -8,6 +8,7 @@
@import 'editor/ide';
@import 'editor/ide-redesign';
@import 'editor/rail';
+@import 'editor/settings';
@import 'editor/toolbar';
@import 'editor/online-users';
@import 'editor/hotkeys';
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/rail.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/rail.scss
index 17a8cd2ba7..ba7f5b8202 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/rail.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/rail.scss
@@ -6,6 +6,11 @@
--ide-rail-link-active-indicator-background: var(--neutral-90);
}
+.ide-rail-tab-button {
+ border: 0;
+ background: none;
+}
+
.ide-rail-tab-link {
border-radius: 12px;
display: block;
@@ -55,3 +60,7 @@
height: 100%;
border: 1px solid var(--border-divider);
}
+
+.ide-rail-tabs-nav {
+ height: 100%;
+}
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/settings.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/settings.scss
new file mode 100644
index 0000000000..75aba658e6
--- /dev/null
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/settings.scss
@@ -0,0 +1,45 @@
+.ide-settings-tab-nav.nav {
+ width: 240px;
+ border-right: var(--bs-modal-header-border-width) solid
+ var(--bs-modal-header-border-color);
+ padding: var(--spacing-02);
+ gap: var(--spacing-02);
+}
+
+.ide-settings-tab-content {
+ padding: var(--spacing-06) var(--spacing-08);
+ max-height: 75%;
+ height: 400px;
+}
+
+.ide-settings-tab-subtitle {
+ font-size: var(--font-size-04);
+ line-height: var(--line-height-03);
+ padding: var(--spacing-06) var(--spacing-08);
+}
+
+.ide-settings-tab-link {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: row;
+ gap: var(--spacing-02);
+ color: var(--neutral-90);
+ padding: var(--spacing-02);
+ border-radius: var(--border-radius-base);
+ font-size: var(--font-size-02);
+ line-height: var(--line-height-02);
+ text-decoration: none;
+
+ &:visited {
+ color: var(--neutral-90);
+ }
+
+ &.active {
+ color: #fff;
+ background-color: var(--neutral-90);
+ }
+}
+
+.ide-settings-modal-body {
+ padding: 0;
+}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index f758c24979..2931f6669c 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -576,6 +576,7 @@
"editor_disconected_click_to_reconnect": "Editor disconnected, click anywhere to reconnect.",
"editor_limit_exceeded_in_this_project": "Too many editors in this project",
"editor_only_hide_pdf": "Editor only <0>(hide PDF)0>",
+ "editor_settings": "Editor settings",
"editor_theme": "Editor theme",
"educational_disclaimer": "I confirm that users will be students or faculty using Overleaf primarily for study and teaching, and can provide evidence of this if requested.",
"educational_disclaimer_heading": "Educational discount confirmation",
@@ -749,6 +750,8 @@
"gallery_page_items_lowercase": "gallery items",
"gallery_page_title": "Gallery - Templates, Examples and Articles written in LaTeX",
"gallery_show_more_tags": "Show more",
+ "general": "General",
+ "general_settings": "General settings",
"generate_token": "Generate token",
"generic_if_problem_continues_contact_us": "If the problem continues please contact us",
"generic_linked_file_compile_error": "This project’s output files are not available because it failed to compile. Please open the project to see the compilation error details.",
@@ -995,6 +998,8 @@
"institutional_login_unknown": "Sorry, we don’t know which institution issued that email address. You can browse our list of institutions to find yours, or you can use one of the other options below.",
"integrations": "Integrations",
"interested_in_cheaper_personal_plan": "Would you be interested in the cheaper <0>__price__0> Personal plan?",
+ "interface": "Interface",
+ "interface_settings": "Interface settings",
"invalid_certificate": "Invalid certificate. Please check the certificate and try again.",
"invalid_confirmation_code": "That didn’t work. Please check the code and try again.",
"invalid_email": "An email address is invalid",
@@ -1468,6 +1473,7 @@
"payment_method_accepted": "__paymentMethod__ accepted",
"payment_provider_unreachable_error": "Sorry, there was an error talking to our payment provider. Please try again in a few moments.\nIf you are using any ad or script blocking extensions in your browser, you may need to temporarily disable them.",
"payment_summary": "Payment summary",
+ "pdf": "PDF",
"pdf_compile_in_progress_error": "A previous compile is still running. Please wait a minute and try compiling again.",
"pdf_compile_rate_limit_hit": "Compile rate limit hit",
"pdf_compile_try_again": "Please wait for your other compile to finish before trying again.",
@@ -1475,6 +1481,7 @@
"pdf_only_hide_editor": "PDF only <0>(hide editor)0>",
"pdf_preview_error": "There was a problem displaying the compilation results for this project.",
"pdf_rendering_error": "PDF Rendering Error",
+ "pdf_settings": "PDF settings",
"pdf_unavailable_for_download": "PDF unavailable for download",
"pdf_viewer": "PDF Viewer",
"pdf_viewer_error": "There was a problem displaying the PDF for this project.",