first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
import SettingsSection from '../settings-section'
import OverallThemeSetting from '../appearance-settings/overall-theme-setting'
import EditorThemeSetting from './editor-theme-setting'
import FontSizeSetting from './font-size-setting'
import FontFamilySetting from './font-family-setting'
import LineHeightSetting from './line-height-setting'
export default function AppearanceSettings() {
return (
<SettingsSection>
<OverallThemeSetting />
<EditorThemeSetting />
<FontSizeSetting />
<FontFamilySetting />
<LineHeightSetting />
</SettingsSection>
)
}

View File

@@ -0,0 +1,46 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import DropdownSetting from '../dropdown-setting'
import getMeta from '@/utils/meta'
import { useMemo } from 'react'
import type { Option } from '../dropdown-setting'
import { useTranslation } from 'react-i18next'
export default function EditorThemeSetting() {
const editorThemes = getMeta('ol-editorThemes')
const legacyEditorThemes = getMeta('ol-legacyEditorThemes')
const { editorTheme, setEditorTheme } = useProjectSettingsContext()
const { t } = useTranslation()
const options = useMemo(() => {
const editorThemeOptions: Array<Option> =
editorThemes?.map(theme => ({
value: theme,
label: theme.replace(/_/g, ' '),
})) ?? []
const dividerOption: Option = {
value: '-',
label: '—————————————————',
disabled: true,
}
const legacyEditorThemeOptions: Array<Option> =
legacyEditorThemes?.map(theme => ({
value: theme,
label: theme.replace(/_/g, ' ') + ' (Legacy)',
})) ?? []
return [...editorThemeOptions, dividerOption, ...legacyEditorThemeOptions]
}, [editorThemes, legacyEditorThemes])
return (
<DropdownSetting
id="editorTheme"
label={t('editor_theme')}
description={t('the_code_editor_color_scheme')}
options={options}
onChange={setEditorTheme}
value={editorTheme}
/>
)
}

View File

@@ -0,0 +1,32 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import { useTranslation } from 'react-i18next'
import DropdownSetting from '../dropdown-setting'
export default function FontFamilySetting() {
const { fontFamily, setFontFamily } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<DropdownSetting
id="fontFamily"
label={t('editor_font_family')}
options={[
{
value: 'monaco',
label: 'Monaco / Menlo / Consolas',
},
{
value: 'lucida',
label: 'Lucida / Source Code Pro',
},
{
value: 'opendyslexicmono',
label: 'OpenDyslexic Mono',
},
]}
onChange={setFontFamily}
value={fontFamily}
width="wide"
/>
)
}

View File

@@ -0,0 +1,24 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import { useTranslation } from 'react-i18next'
import DropdownSetting from '../dropdown-setting'
const sizes = [10, 11, 12, 13, 14, 16, 18, 20, 22, 24]
const options = sizes.map(size => ({
value: size,
label: `${size}px`,
}))
export default function FontSizeSetting() {
const { fontSize, setFontSize } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<DropdownSetting
id="fontSize"
label={t('editor_font_size')}
options={options}
onChange={setFontSize}
value={fontSize}
/>
)
}

View File

@@ -0,0 +1,31 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import { useTranslation } from 'react-i18next'
import DropdownSetting from '../dropdown-setting'
export default function LineHeightSetting() {
const { lineHeight, setLineHeight } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<DropdownSetting
id="lineHeight"
label={t('editor_line_height')}
options={[
{
value: 'compact',
label: t('compact'),
},
{
value: 'normal',
label: t('normal'),
},
{
value: 'wide',
label: t('wide'),
},
]}
onChange={setLineHeight}
value={lineHeight}
/>
)
}

View File

@@ -0,0 +1,45 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import DropdownSetting from '../dropdown-setting'
import getMeta from '@/utils/meta'
import { useMemo } from 'react'
import type { Option } from '../dropdown-setting'
import { useTranslation } from 'react-i18next'
import { OverallThemeMeta } from '../../../../../../../types/project-settings'
import { isIEEEBranded } from '@/utils/is-ieee-branded'
import { useLayoutContext } from '@/shared/context/layout-context'
import { OverallTheme } from '@/shared/utils/styles'
export default function OverallThemeSetting() {
const { t } = useTranslation()
const overallThemes = getMeta('ol-overallThemes') as
| OverallThemeMeta[]
| undefined
const { loadingStyleSheet } = useLayoutContext()
const { overallTheme, setOverallTheme } = useProjectSettingsContext()
const options: Array<Option<OverallTheme>> = useMemo(
() =>
overallThemes?.map(({ name, val }) => ({
value: val,
label: name,
})) ?? [],
[overallThemes]
)
if (!overallThemes || isIEEEBranded()) {
return null
}
return (
<DropdownSetting
id="overallTheme"
label={t('overall_theme')}
description={t('the_overleaf_color_scheme')}
options={options}
onChange={setOverallTheme}
value={overallTheme}
loading={loadingStyleSheet}
/>
)
}

View File

@@ -0,0 +1,32 @@
import OLButton from '@/features/ui/components/ol/ol-button'
import Setting from './setting'
export default function ButtonSetting({
id,
label,
buttonText,
onClick,
description,
disabled,
}: {
id: string
label: string
buttonText: string
onClick: () => void
description?: string
disabled?: boolean
}) {
return (
<Setting controlId={id} label={label} description={description}>
<OLButton
id={id}
variant="secondary"
size="sm"
onClick={onClick}
disabled={disabled}
>
{buttonText}
</OLButton>
</Setting>
)
}

View File

@@ -0,0 +1,31 @@
import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
import { useCallback } from 'react'
import * as eventTracking from '../../../../../infrastructure/event-tracking'
export default function AutoCompileSetting() {
const { autoCompile, setAutoCompile } = useCompileContext()
const { t } = useTranslation()
const sendEventAndSet = useCallback(
(value: boolean) => {
eventTracking.sendMB('recompile-setting-changed', {
setting: 'auto-compile',
settingVal: value,
})
setAutoCompile(value)
},
[setAutoCompile]
)
return (
<ToggleSetting
id="autoCompile"
label={t('autocompile')}
description={t('automatically_recompile_the_project_as_you_edit')}
checked={autoCompile}
onChange={sendEventAndSet}
/>
)
}

View File

@@ -0,0 +1,43 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import DropdownSetting from '../dropdown-setting'
import type { Option } from '../dropdown-setting'
import { useTranslation } from 'react-i18next'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import { ProjectCompiler } from '../../../../../../../types/project-settings'
const OPTIONS: Option<ProjectCompiler>[] = [
{
value: 'pdflatex',
label: 'pdfLaTeX',
},
{
value: 'latex',
label: 'LaTeX',
},
{
value: 'xelatex',
label: 'XeLaTeX',
},
{
value: 'lualatex',
label: 'LuaLaTeX',
},
]
export default function CompilerSetting() {
const { compiler, setCompiler } = useProjectSettingsContext()
const { t } = useTranslation()
const { write } = usePermissionsContext()
return (
<DropdownSetting
id="compiler"
label={t('compiler')}
description={t('the_latex_engine_used_for_compiling')}
disabled={!write}
options={OPTIONS}
onChange={setCompiler}
value={compiler}
/>
)
}

View File

@@ -0,0 +1,22 @@
import SettingsSection from '../settings-section'
import AutoCompileSetting from './auto-compile-setting'
import CompilerSetting from './compiler-setting'
import DraftSetting from './draft-setting'
import ImageNameSetting from './image-name-setting'
import RootDocumentSetting from './root-document-setting'
import StopOnFirstErrorSetting from './stop-on-first-error-setting'
export default function CompilerSettings() {
return (
<>
<SettingsSection>
<RootDocumentSetting />
<CompilerSetting />
<ImageNameSetting />
<DraftSetting />
<StopOnFirstErrorSetting />
<AutoCompileSetting />
</SettingsSection>
</>
)
}

View File

@@ -0,0 +1,43 @@
import { useTranslation } from 'react-i18next'
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
import { useCallback, useMemo } from 'react'
import * as eventTracking from '../../../../../infrastructure/event-tracking'
import DropdownSetting from '../dropdown-setting'
export default function DraftSetting() {
const { draft, setDraft } = useCompileContext()
const { t } = useTranslation()
const sendEventAndSet = useCallback(
(value: boolean) => {
eventTracking.sendMB('recompile-setting-changed', {
setting: 'compile-mode',
settingVal: value,
})
setDraft(value)
},
[setDraft]
)
const options = useMemo(
() => [
{ label: t('normal'), value: false },
{
label: t('fast_draft'),
value: true,
},
],
[t]
)
return (
<DropdownSetting
id="draft"
label={t('compile_mode')}
options={options}
description={t('switch_compile_mode_for_faster_draft_compilation')}
value={draft}
onChange={sendEventAndSet}
/>
)
}

View File

@@ -0,0 +1,43 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import DropdownSetting from '../dropdown-setting'
import type { Option } from '../dropdown-setting'
import { useTranslation } from 'react-i18next'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import { useMemo } from 'react'
import getMeta from '@/utils/meta'
export default function ImageNameSetting() {
const { imageName, setImageName } = useProjectSettingsContext()
const { t } = useTranslation()
const { write } = usePermissionsContext()
const allowedImageNames = useMemo(
() => getMeta('ol-allowedImageNames') || [],
[]
)
const options: Array<Option> = useMemo(
() =>
allowedImageNames.map(({ imageName, imageDesc }) => ({
value: imageName,
label: imageDesc,
})),
[allowedImageNames]
)
if (allowedImageNames.length === 0) {
return null
}
return (
<DropdownSetting
id="imageName"
label={t('tex_live_version')}
description={t('the_version_of_tex_live_used_for_compiling')}
disabled={!write}
options={options}
onChange={setImageName}
value={imageName}
/>
)
}

View File

@@ -0,0 +1,49 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import DropdownSetting from '../dropdown-setting'
import { useMemo } from 'react'
import type { Option } from '../dropdown-setting'
import { useTranslation } from 'react-i18next'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { isValidTeXFile } from '@/main/is-valid-tex-file'
export default function RootDocumentSetting() {
const { rootDocId, setRootDocId } = useProjectSettingsContext()
const { t } = useTranslation()
const { write } = usePermissionsContext()
const { docs } = useFileTreeData()
const validDocsOptions = useMemo(() => {
const filteredDocs =
docs?.filter(
doc => isValidTeXFile(doc.doc.name) || rootDocId === doc.doc.id
) ?? []
const mappedDocs: Array<Option> = filteredDocs.map(doc => ({
value: doc.doc.id,
label: doc.path,
}))
if (!rootDocId) {
mappedDocs.unshift({
value: '',
label: 'None',
disabled: true,
})
}
return mappedDocs
}, [docs, rootDocId])
return (
<DropdownSetting
id="rootDocId"
label={t('main_document')}
description={t('the_primary_file_for_compiling_your_project')}
disabled={!write}
options={validDocsOptions}
onChange={setRootDocId}
value={rootDocId}
/>
)
}

View File

@@ -0,0 +1,18 @@
import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
export default function StopOnFirstErrorSetting() {
const { stopOnFirstError, setStopOnFirstError } = useCompileContext()
const { t } = useTranslation()
return (
<ToggleSetting
id="stopOnFirstError"
label={t('stop_on_first_error')}
description={t('identify_errors_with_your_compile')}
checked={stopOnFirstError}
onChange={setStopOnFirstError}
/>
)
}

View File

@@ -0,0 +1,108 @@
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
import { ChangeEventHandler, useCallback } from 'react'
import Setting from './setting'
import classNames from 'classnames'
import { Spinner } from 'react-bootstrap-5'
type PossibleValue = string | number | boolean
export type Option<T extends PossibleValue = string> = {
value: T
label: string
ariaHidden?: 'true' | 'false'
disabled?: boolean
}
export type Optgroup<T extends PossibleValue = string> = {
label: string
options: Array<Option<T>>
}
type SettingsMenuSelectProps<T extends PossibleValue = string> = {
id: string
label: string
options: Array<Option<T>>
onChange: (val: T) => void
description?: string
// TODO: We can remove optgroup when the spellcheck setting is
// split into 2 and no longer uses it.
optgroup?: Optgroup<T>
value?: T
disabled?: boolean
width?: 'default' | 'wide'
loading?: boolean
}
export default function DropdownSetting<T extends PossibleValue = string>({
id,
label,
options,
onChange,
value,
optgroup,
description = undefined,
disabled = false,
width = 'default',
loading = false,
}: SettingsMenuSelectProps<T>) {
const handleChange: ChangeEventHandler<HTMLSelectElement> = useCallback(
event => {
const selectedValue = event.target.value
let onChangeValue: PossibleValue = selectedValue
if (typeof value === 'boolean') {
onChangeValue = selectedValue === 'true'
} else if (typeof value === 'number') {
onChangeValue = parseInt(selectedValue, 10)
}
onChange(onChangeValue as T)
},
[onChange, value]
)
return (
<Setting controlId={id} label={label} description={description}>
{loading ? (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
) : (
<OLFormSelect
id={id}
className={classNames('ide-dropdown-setting', {
'ide-dropdown-setting-wide': width === 'wide',
})}
size="sm"
onChange={handleChange}
value={value?.toString()}
disabled={disabled}
>
{options.map(option => (
<option
key={`${id}-${option.value}`}
value={option.value.toString()}
aria-hidden={option.ariaHidden}
disabled={option.disabled}
>
{option.label}
</option>
))}
{optgroup ? (
<optgroup label={optgroup.label}>
{optgroup.options.map(option => (
<option
value={option.value.toString()}
key={option.value.toString()}
>
{option.label}
</option>
))}
</optgroup>
) : null}
</OLFormSelect>
)}
</Setting>
)
}

View File

@@ -0,0 +1,19 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
export default function AutoCloseBracketsSetting() {
const { autoPairDelimiters, setAutoPairDelimiters } =
useProjectSettingsContext()
const { t } = useTranslation()
return (
<ToggleSetting
id="autoPairDelimiters"
label={t('auto_close_brackets')}
description={t('automatically_insert_closing_brackets_and_parentheses')}
checked={autoPairDelimiters}
onChange={setAutoPairDelimiters}
/>
)
}

View File

@@ -0,0 +1,18 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
export default function AutoCompleteSetting() {
const { autoComplete, setAutoComplete } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<ToggleSetting
id="autoComplete"
label={t('auto_complete')}
description={t('suggests_code_completions_while_typing')}
checked={autoComplete}
onChange={setAutoComplete}
/>
)
}

View File

@@ -0,0 +1,18 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
export default function CodeCheckSetting() {
const { syntaxValidation, setSyntaxValidation } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<ToggleSetting
id="syntaxValidation"
label={t('syntax_validation')}
description={t('enables_real_time_syntax_checking_in_the_editor')}
checked={syntaxValidation}
onChange={setSyntaxValidation}
/>
)
}

View File

@@ -0,0 +1,29 @@
import { useTranslation } from 'react-i18next'
import { useCallback } from 'react'
import ButtonSetting from '../button-setting'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useRailContext } from '@/features/ide-redesign/contexts/rail-context'
export default function DictionarySetting() {
const { t } = useTranslation()
const { setActiveModal } = useRailContext()
// TODO ide-redesign-cleanup: leftMenu is a misnomer, in the
// redesign it refers to the settings modal
const { setLeftMenuShown } = useLayoutContext()
const onClick = useCallback(() => {
setActiveModal('dictionary')
setLeftMenuShown(false)
}, [setLeftMenuShown, setActiveModal])
return (
<ButtonSetting
id="dictionary-settings"
label={t('dictionary')}
description={t('edit_your_custom_dictionary')}
buttonText={t('edit')}
onClick={onClick}
/>
)
}

View File

@@ -0,0 +1,10 @@
import DictionaryModal from '@/features/dictionary/components/dictionary-modal'
import { useRailContext } from '@/features/ide-redesign/contexts/rail-context'
import { useCallback } from 'react'
export default function DictionarySettingsModal({ show }: { show: boolean }) {
const { setActiveModal } = useRailContext()
const handleHide = useCallback(() => setActiveModal(null), [setActiveModal])
return <DictionaryModal show={show} handleHide={handleHide} />
}

View File

@@ -0,0 +1,40 @@
import AutoCompleteSetting from './auto-complete-setting'
import CodeCheckSetting from './code-check-setting'
import AutoCloseBracketsSetting from './auto-close-brackets-setting'
import SettingsSection from '../settings-section'
import MathPreviewSetting from './math-preview-setting'
import { useTranslation } from 'react-i18next'
import KeybindingSetting from './keybinding-setting'
import PDFViewerSetting from './pdf-viewer-setting'
import SpellCheckSetting from './spell-check-setting'
import DictionarySetting from './dictionary-setting'
import importOverleafModules from '../../../../../../macros/import-overleaf-module.macro'
const [referenceSearchSettingModule] = importOverleafModules(
'referenceSearchSetting'
)
const ReferenceSearchSetting = referenceSearchSettingModule?.import.default
export default function EditorSettings() {
const { t } = useTranslation()
return (
<>
<SettingsSection>
<AutoCompleteSetting />
<AutoCloseBracketsSetting />
<CodeCheckSetting />
<KeybindingSetting />
<PDFViewerSetting />
{ReferenceSearchSetting && <ReferenceSearchSetting />}
</SettingsSection>
<SettingsSection title={t('spellcheck')}>
<SpellCheckSetting />
<DictionarySetting />
</SettingsSection>
<SettingsSection title={t('tools')}>
<MathPreviewSetting />
</SettingsSection>
</>
)
}

View File

@@ -0,0 +1,35 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import { useTranslation } from 'react-i18next'
import DropdownSetting from '../dropdown-setting'
import { Keybindings } from '../../../../../../../types/user-settings'
const OPTIONS: { value: Keybindings; label: string }[] = [
{
value: 'default',
label: 'None',
},
{
value: 'vim',
label: 'Vim',
},
{
value: 'emacs',
label: 'Emacs',
},
]
export default function KeybindingSetting() {
const { mode, setMode } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<DropdownSetting
id="mode"
label={t('keybindings')}
description={t('work_in_vim_or_emacs_emulation_mode')}
options={OPTIONS}
onChange={setMode}
value={mode}
/>
)
}

View File

@@ -0,0 +1,18 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
export default function MathPreviewSetting() {
const { mathPreview, setMathPreview } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<ToggleSetting
id="mathPreview"
label={t('equation_preview')}
description={t('show_live_equation_previews_while_typing')}
checked={mathPreview}
onChange={setMathPreview}
/>
)
}

View File

@@ -0,0 +1,27 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import { useTranslation } from 'react-i18next'
import DropdownSetting from '../dropdown-setting'
export default function PDFViewerSetting() {
const { pdfViewer, setPdfViewer } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<DropdownSetting
id="pdfViewer"
label={t('pdf_viewer')}
options={[
{
value: 'pdfjs',
label: t('overleaf'),
},
{
value: 'native',
label: t('browser'),
},
]}
onChange={setPdfViewer}
value={pdfViewer}
/>
)
}

View File

@@ -0,0 +1,40 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import { useTranslation } from 'react-i18next'
import DropdownSetting, { Optgroup } from '../dropdown-setting'
import { useMemo } from 'react'
import getMeta from '@/utils/meta'
import { supportsWebAssembly } from '@/utils/wasm'
// TODO: Split this into separate setttings for spell check
// language and spell check on/off
export default function SpellCheckSetting() {
const { spellCheckLanguage, setSpellCheckLanguage } =
useProjectSettingsContext()
const { t } = useTranslation()
const optgroup: Optgroup = useMemo(() => {
const options = (getMeta('ol-languages') ?? [])
// only include spell-check languages that are available in the client
.filter(language => language.dic !== undefined)
return {
label: 'Language',
options: options.map(language => ({
value: language.code,
label: language.name,
})),
}
}, [])
return (
<DropdownSetting
id="spellCheckLanguage"
label={t('spellcheck_language')}
options={[{ value: '', label: t('off') }]}
optgroup={optgroup}
onChange={setSpellCheckLanguage}
value={supportsWebAssembly() ? spellCheckLanguage : ''}
width="wide"
/>
)
}

View File

@@ -0,0 +1,25 @@
export default function Setting({
label,
controlId,
children,
description = undefined,
}: {
label: string
description: string | undefined
controlId: string
children: React.ReactNode
}) {
return (
<div className="ide-setting">
<div>
<label htmlFor={controlId} className="ide-setting-title">
{label}
</label>
{description && (
<div className="ide-setting-description">{description}</div>
)}
</div>
{children}
</div>
)
}

View File

@@ -0,0 +1,138 @@
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'
import EditorSettings from './editor-settings/editor-settings'
import AppearanceSettings from './appearance-settings/appearance-settings'
import CompilerSettings from './compiler-settings/compiler-settings'
export type SettingsEntry = SettingsLink | SettingsTab
type SettingsTab = {
icon: AvailableUnfilledIcon
key: string
component: ReactElement
title: string
}
type SettingsLink = {
key: string
icon: AvailableUnfilledIcon
href: string
title: string
}
export const SettingsModalBody = () => {
const { t } = useTranslation()
const settingsTabs: SettingsEntry[] = useMemo(
() => [
{
key: 'editor',
title: t('editor'),
icon: 'code',
component: <EditorSettings />,
},
{
key: 'compiler',
title: t('compiler'),
icon: 'picture_as_pdf',
component: <CompilerSettings />,
},
{
key: 'appearance',
title: t('appearance'),
icon: 'brush',
component: <AppearanceSettings />,
},
{
key: 'account_settings',
title: t('account_settings'),
icon: 'settings',
href: '/user/settings',
},
],
[t]
)
const [activeTab, setActiveTab] = useState<string | null | undefined>(
settingsTabs[0]?.key
)
return (
<TabContainer
transition={false}
onSelect={setActiveTab}
defaultActiveKey={activeTab ?? undefined}
id="ide-settings-tabs"
>
<div className="d-flex flex-row">
<Nav
defaultActiveKey={settingsTabs[0]?.key}
className="d-flex flex-column ide-settings-tab-nav"
>
{settingsTabs.map(entry => (
<SettingsNavLink entry={entry} key={entry.key} />
))}
</Nav>
<TabContent className="ide-settings-tab-content">
{settingsTabs
.filter(t => 'component' in t)
.map(({ key, component }) => (
<TabPane eventKey={key} key={key}>
{component}
</TabPane>
))}
</TabContent>
</div>
</TabContainer>
)
}
const SettingsNavLink = ({ entry }: { entry: SettingsEntry }) => {
if ('href' in entry) {
return (
<a
href={entry.href}
target="_blank"
rel="noopener"
className="ide-settings-tab-link"
>
<MaterialIcon
className="ide-settings-tab-link-icon"
type={entry.icon}
unfilled
/>
<span>{entry.title}</span>
<div className="flex-grow-1" />
<MaterialIcon
type="open_in_new"
className="ide-settings-tab-link-external"
/>
</a>
)
} else {
return (
<>
<NavLink
eventKey={entry.key}
className="ide-settings-tab-link"
key={entry.key}
>
<MaterialIcon
className="ide-settings-tab-link-icon"
type={entry.icon}
unfilled
/>
<span>{entry.title}</span>
</NavLink>
</>
)
}
}

View File

@@ -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 (
<OLModal
show={leftMenuShown}
onHide={() => setLeftMenuShown(false)}
size="lg"
>
<OLModalHeader closeButton>
<OLModalTitle>{t('settings')}</OLModalTitle>
</OLModalHeader>
<OLModalBody className="ide-settings-modal-body">
<SettingsModalBody />
</OLModalBody>
</OLModal>
)
}
export default SettingsModal

View File

@@ -0,0 +1,18 @@
export default function SettingsSection({
children,
title,
}: {
children: React.ReactNode | React.ReactNode[]
title?: string
}) {
if (!children) {
return null
}
return (
<div className="ide-settings-section">
{title && <div className="ide-settings-section-title">{title}</div>}
{children}
</div>
)
}

View File

@@ -0,0 +1,34 @@
import Setting from './setting'
import OLFormSwitch from '@/features/ui/components/ol/ol-form-switch'
export default function ToggleSetting({
id,
label,
description,
checked,
onChange,
disabled,
}: {
id: string
label: string
description: string
checked: boolean | undefined
onChange: (newValue: boolean) => void
disabled?: boolean
}) {
const handleChange = () => {
onChange(!checked)
}
return (
<Setting controlId={id} label={label} description={description}>
<OLFormSwitch
id={id}
onChange={handleChange}
checked={checked}
label={label}
disabled={disabled}
/>
</Setting>
)
}