first commit
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user