Merge pull request #20740 from overleaf/rd-ide-offcanvas

[web] Implement the editor's left menu in Offcanvas

GitOrigin-RevId: 999e995d664b1dc958f56643f05e95b8aa2d6290
This commit is contained in:
Rebeka Dekany 2024-10-11 11:22:38 +02:00 committed by Copybot
parent 7a26d46d7c
commit f8efc3e2ae
40 changed files with 865 additions and 239 deletions

View file

@ -29,4 +29,12 @@
direction: ltr; direction: ltr;
font-feature-settings: 'liga'; font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
&.size-2x {
font-size: 2em;
}
&.rotate-180 {
transform: rotate(180deg);
}
} }

View file

@ -1,12 +1,20 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, Button, Modal } from 'react-bootstrap'
import Icon from '../../../shared/components/icon'
import Tooltip from '../../../shared/components/tooltip'
import useAsync from '../../../shared/hooks/use-async' import useAsync from '../../../shared/hooks/use-async'
import { postJSON } from '../../../infrastructure/fetch-json' import { postJSON } from '../../../infrastructure/fetch-json'
import ignoredWords from '../ignored-words' import ignoredWords from '../ignored-words'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLNotification from '@/features/ui/components/ol/ol-notification'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type DictionaryModalContentProps = { type DictionaryModalContentProps = {
handleHide: () => void handleHide: () => void
@ -42,13 +50,16 @@ export default function DictionaryModalContent({
return ( return (
<> <>
<Modal.Header closeButton> <OLModalHeader closeButton>
<Modal.Title>{t('edit_dictionary')}</Modal.Title> <OLModalTitle>{t('edit_dictionary')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body> <OLModalBody>
{isError ? ( {isError ? (
<Alert bsStyle="danger">{t('generic_something_went_wrong')}</Alert> <OLNotification
type="error"
content={t('generic_something_went_wrong')}
/>
) : null} ) : null}
{learnedWords?.size > 0 ? ( {learnedWords?.size > 0 ? (
@ -56,22 +67,25 @@ export default function DictionaryModalContent({
{[...learnedWords].sort(wordsSortFunction).map(learnedWord => ( {[...learnedWords].sort(wordsSortFunction).map(learnedWord => (
<li key={learnedWord} className="dictionary-entry"> <li key={learnedWord} className="dictionary-entry">
<span className="dictionary-entry-name">{learnedWord}</span> <span className="dictionary-entry-name">{learnedWord}</span>
<Tooltip <OLTooltip
id={`tooltip-remove-learned-word-${learnedWord}`} id={`tooltip-remove-learned-word-${learnedWord}`}
description={t('edit_dictionary_remove')} description={t('edit_dictionary_remove')}
overlayProps={{ delay: 0 }} overlayProps={{ delay: 0 }}
> >
<Button <OLIconButton
bsStyle="danger" variant="danger"
bsSize="xs" size="sm"
onClick={() => handleRemove(learnedWord)} onClick={() => handleRemove(learnedWord)}
> bs3Props={{ bsSize: 'xsmall' }}
<Icon icon={
type="trash-o" bsVersion({
accessibilityLabel={t('edit_dictionary_remove')} bs5: 'delete',
/> bs3: 'trash-o',
</Button> }) as string
</Tooltip> }
accessibilityLabel={t('edit_dictionary_remove')}
/>
</OLTooltip>
</li> </li>
))} ))}
</ul> </ul>
@ -80,13 +94,13 @@ export default function DictionaryModalContent({
<i>{t('edit_dictionary_empty')}</i> <i>{t('edit_dictionary_empty')}</i>
</p> </p>
)} )}
</Modal.Body> </OLModalBody>
<Modal.Footer> <OLModalFooter>
<Button bsStyle={null} className="btn-secondary" onClick={handleHide}> <OLButton variant="secondary" onClick={handleHide}>
{t('close')} {t('close')}
</Button> </OLButton>
</Modal.Footer> </OLModalFooter>
</> </>
) )
} }

View file

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import DictionaryModalContent from './dictionary-modal-content' import DictionaryModalContent from './dictionary-modal-content'
import AccessibleModal from '../../../shared/components/accessible-modal'
import withErrorBoundary from '../../../infrastructure/error-boundary' import withErrorBoundary from '../../../infrastructure/error-boundary'
import OLModal from '@/features/ui/components/ol/ol-modal'
type DictionaryModalProps = { type DictionaryModalProps = {
show?: boolean show?: boolean
@ -10,15 +10,15 @@ type DictionaryModalProps = {
function DictionaryModal({ show, handleHide }: DictionaryModalProps) { function DictionaryModal({ show, handleHide }: DictionaryModalProps) {
return ( return (
<AccessibleModal <OLModal
animation animation
show={show} show={show}
onHide={handleHide} onHide={handleHide}
id="dictionary-modal" id="dictionary-modal"
bsSize="small" size="sm"
> >
<DictionaryModalContent handleHide={handleHide} /> <DictionaryModalContent handleHide={handleHide} />
</AccessibleModal> </OLModal>
) )
} }

View file

@ -4,6 +4,7 @@ import EditorCloneProjectModalWrapper from '../../clone-project-modal/components
import LeftMenuButton from './left-menu-button' import LeftMenuButton from './left-menu-button'
import { useLocation } from '../../../shared/hooks/use-location' import { useLocation } from '../../../shared/hooks/use-location'
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '../../../infrastructure/event-tracking'
import { bsVersionIcon } from '@/features/utils/bootstrap-5'
type ProjectCopyResponse = { type ProjectCopyResponse = {
project_id: string project_id: string
@ -30,10 +31,10 @@ export default function ActionsCopyProject() {
<> <>
<LeftMenuButton <LeftMenuButton
onClick={handleShowModal} onClick={handleShowModal}
icon={{ icon={bsVersionIcon({
type: 'copy', bs5: { type: 'file_copy' },
fw: true, bs3: { type: 'copy', fw: true },
}} })}
> >
{t('copy_project')} {t('copy_project')}
</LeftMenuButton> </LeftMenuButton>

View file

@ -1,10 +1,11 @@
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Tooltip from '../../../shared/components/tooltip'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import WordCountModal from '../../word-count-modal/components/word-count-modal' import WordCountModal from '../../word-count-modal/components/word-count-modal'
import LeftMenuButton from './left-menu-button' import LeftMenuButton from './left-menu-button'
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '../../../infrastructure/event-tracking'
import { bsVersionIcon } from '@/features/utils/bootstrap-5'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
export default function ActionsWordCount() { export default function ActionsWordCount() {
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
@ -21,15 +22,15 @@ export default function ActionsWordCount() {
{pdfUrl ? ( {pdfUrl ? (
<LeftMenuButton <LeftMenuButton
onClick={handleShowModal} onClick={handleShowModal}
icon={{ icon={bsVersionIcon({
type: 'eye', bs5: { type: 'match_case' },
fw: true, bs3: { type: 'eye', fw: true },
}} })}
> >
{t('word_count')} {t('word_count')}
</LeftMenuButton> </LeftMenuButton>
) : ( ) : (
<Tooltip <OLTooltip
id="disabled-word-count" id="disabled-word-count"
description={t('please_compile_pdf_before_word_count')} description={t('please_compile_pdf_before_word_count')}
overlayProps={{ overlayProps={{
@ -39,10 +40,10 @@ export default function ActionsWordCount() {
{/* OverlayTrigger won't fire unless the child is a non-react html element (e.g div, span) */} {/* OverlayTrigger won't fire unless the child is a non-react html element (e.g div, span) */}
<div> <div>
<LeftMenuButton <LeftMenuButton
icon={{ icon={bsVersionIcon({
type: 'eye', bs5: { type: 'match_case' },
fw: true, bs3: { type: 'eye', fw: true },
}} })}
disabled disabled
disabledAccesibilityText={t( disabledAccesibilityText={t(
'please_compile_pdf_before_word_count' 'please_compile_pdf_before_word_count'
@ -51,7 +52,7 @@ export default function ActionsWordCount() {
{t('word_count')} {t('word_count')}
</LeftMenuButton> </LeftMenuButton>
</div> </div>
</Tooltip> </OLTooltip>
)} )}
<WordCountModal show={showModal} handleHide={() => setShowModal(false)} /> <WordCountModal show={showModal} handleHide={() => setShowModal(false)} />
</> </>

View file

@ -2,9 +2,11 @@ import { useTranslation } from 'react-i18next'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '../../../shared/context/project-context'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import Tooltip from '../../../shared/components/tooltip'
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '../../../infrastructure/event-tracking'
import { isSmallDevice } from '../../../infrastructure/event-tracking' import { isSmallDevice } from '../../../infrastructure/event-tracking'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
export default function DownloadPDF() { export default function DownloadPDF() {
const { t } = useTranslation() const { t } = useTranslation()
@ -27,24 +29,30 @@ export default function DownloadPDF() {
rel="noreferrer" rel="noreferrer"
onClick={sendDownloadEvent} onClick={sendDownloadEvent}
> >
<Icon type="file-pdf-o" modifier="2x" /> <BootstrapVersionSwitcher
bs3={<Icon type="file-pdf-o" modifier="2x" />}
bs5={<MaterialIcon type="picture_as_pdf" size="2x" />}
/>
<br /> <br />
PDF PDF
</a> </a>
) )
} else { } else {
return ( return (
<Tooltip <OLTooltip
id="disabled-pdf-download" id="disabled-pdf-download"
description={t('please_compile_pdf_before_download')} description={t('please_compile_pdf_before_download')}
overlayProps={{ placement: 'bottom' }} overlayProps={{ placement: 'bottom' }}
> >
<div className="link-disabled"> <div className="link-disabled">
<Icon type="file-pdf-o" modifier="2x" /> <BootstrapVersionSwitcher
bs3={<Icon type="file-pdf-o" modifier="2x" />}
bs5={<MaterialIcon type="picture_as_pdf" size="2x" />}
/>
<br /> <br />
PDF PDF
</div> </div>
</Tooltip> </OLTooltip>
) )
} }
} }

View file

@ -3,6 +3,8 @@ import { useProjectContext } from '../../../shared/context/project-context'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '../../../infrastructure/event-tracking'
import { isSmallDevice } from '../../../infrastructure/event-tracking' import { isSmallDevice } from '../../../infrastructure/event-tracking'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
export default function DownloadSource() { export default function DownloadSource() {
const { t } = useTranslation() const { t } = useTranslation()
@ -23,7 +25,10 @@ export default function DownloadSource() {
rel="noreferrer" rel="noreferrer"
onClick={sendDownloadEvent} onClick={sendDownloadEvent}
> >
<Icon type="file-archive-o" modifier="2x" /> <BootstrapVersionSwitcher
bs3={<Icon type="file-archive-o" modifier="2x" />}
bs5={<MaterialIcon type="folder_zip" size="2x" />}
/>
<br /> <br />
{t('source')} {t('source')}
</a> </a>

View file

@ -5,35 +5,63 @@ import { Modal } from 'react-bootstrap'
import classNames from 'classnames' import classNames from 'classnames'
import { lazy, memo, Suspense } from 'react' import { lazy, memo, Suspense } from 'react'
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner' import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { Offcanvas } from 'react-bootstrap-5'
const EditorLeftMenuBody = lazy(() => import('./editor-left-menu-body')) const EditorLeftMenuBody = lazy(() => import('./editor-left-menu-body'))
function EditorLeftMenu() { function EditorLeftMenu() {
const { leftMenuShown, setLeftMenuShown } = useLayoutContext() const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
const closeModal = () => { const closeLeftMenu = () => {
setLeftMenuShown(false) setLeftMenuShown(false)
} }
return ( return (
<> <BootstrapVersionSwitcher
<AccessibleModal bs3={
backdropClassName="left-menu-modal-backdrop" <>
keyboard <AccessibleModal
onHide={closeModal} backdropClassName="left-menu-modal-backdrop"
id="left-menu-modal" keyboard
show={leftMenuShown} onHide={closeLeftMenu}
> id="left-menu-modal"
<Modal.Body show={leftMenuShown}
className={classNames('full-size', { shown: leftMenuShown })} >
id="left-menu" <Modal.Body
> className={classNames('full-size', { shown: leftMenuShown })}
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}> id="left-menu"
<EditorLeftMenuBody /> >
</Suspense> <Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
</Modal.Body> <EditorLeftMenuBody />
</AccessibleModal> </Suspense>
{leftMenuShown && <LeftMenuMask />} </Modal.Body>
</> </AccessibleModal>
{leftMenuShown && <LeftMenuMask />}
</>
}
bs5={
<>
<Offcanvas
show={leftMenuShown}
onHide={closeLeftMenu}
backdropClassName="left-menu-modal-backdrop"
id="left-menu-offcanvas"
>
<Offcanvas.Body
className={classNames('full-size', 'left-menu', {
shown: leftMenuShown,
})}
id="left-menu"
>
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
<EditorLeftMenuBody />
</Suspense>
</Offcanvas.Body>
</Offcanvas>
{leftMenuShown && <LeftMenuMask />}
</>
}
/>
) )
} }

View file

@ -3,6 +3,7 @@ import { useCallback } from 'react'
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '../../../infrastructure/event-tracking'
import { useContactUsModal } from '../../../shared/hooks/use-contact-us-modal' import { useContactUsModal } from '../../../shared/hooks/use-contact-us-modal'
import LeftMenuButton from './left-menu-button' import LeftMenuButton from './left-menu-button'
import { bsVersionIcon } from '@/features/utils/bootstrap-5'
export default function HelpContactUs() { export default function HelpContactUs() {
const { modal, showModal } = useContactUsModal() const { modal, showModal } = useContactUsModal()
@ -17,10 +18,10 @@ export default function HelpContactUs() {
<> <>
<LeftMenuButton <LeftMenuButton
onClick={showModalWithAnalytics} onClick={showModalWithAnalytics}
icon={{ icon={bsVersionIcon({
type: 'question', bs5: { type: 'contact_support' },
fw: true, bs3: { type: 'question', fw: true },
}} })}
> >
{t('contact_us')} {t('contact_us')}
</LeftMenuButton> </LeftMenuButton>

View file

@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import LeftMenuButton from './left-menu-button' import LeftMenuButton from './left-menu-button'
import { bsVersionIcon } from '@/features/utils/bootstrap-5'
export default function HelpDocumentation() { export default function HelpDocumentation() {
const { t } = useTranslation() const { t } = useTranslation()
@ -9,10 +10,10 @@ export default function HelpDocumentation() {
<LeftMenuButton <LeftMenuButton
type="link" type="link"
href="/learn" href="/learn"
icon={{ icon={bsVersionIcon({
type: 'book', bs5: { type: 'book_4' },
fw: true, bs3: { type: 'book', fw: true },
}} })}
> >
{t('documentation')} {t('documentation')}
</LeftMenuButton> </LeftMenuButton>

View file

@ -4,6 +4,7 @@ import * as eventTracking from '../../../infrastructure/event-tracking'
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '../../../shared/context/project-context'
import HotkeysModal from '../../hotkeys-modal/components/hotkeys-modal' import HotkeysModal from '../../hotkeys-modal/components/hotkeys-modal'
import LeftMenuButton from './left-menu-button' import LeftMenuButton from './left-menu-button'
import { bsVersionIcon } from '@/features/utils/bootstrap-5'
export default function HelpShowHotkeys() { export default function HelpShowHotkeys() {
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
@ -20,10 +21,10 @@ export default function HelpShowHotkeys() {
<> <>
<LeftMenuButton <LeftMenuButton
onClick={showModalWithAnalytics} onClick={showModalWithAnalytics}
icon={{ icon={bsVersionIcon({
type: 'keyboard-o', bs5: { type: 'keyboard' },
fw: true, bs3: { type: 'keyboard-o', fw: true },
}} })}
> >
{t('show_hotkeys')} {t('show_hotkeys')}
</LeftMenuButton> </LeftMenuButton>

View file

@ -1,20 +1,43 @@
import { PropsWithChildren } from 'react' import { PropsWithChildren } from 'react'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
type Props = { type Props = {
onClick?: () => void onClick?: () => void
icon: { icon?: {
type: string type: string
fw?: boolean fw?: boolean
} }
svgIcon?: React.ReactElement | null
disabled?: boolean disabled?: boolean
disabledAccesibilityText?: string disabledAccesibilityText?: string
type?: 'button' | 'link' type?: 'button' | 'link'
href?: string href?: string
} }
function LeftMenuButtonIcon({
svgIcon,
icon,
}: {
svgIcon?: React.ReactElement | null
icon?: { type: string; fw?: boolean }
}) {
if (svgIcon) {
return <div className="material-symbols">{svgIcon}</div>
} else if (icon) {
return (
<BootstrapVersionSwitcher
bs3={<Icon type={icon.type} fw={icon.fw ?? false} />}
bs5={<MaterialIcon type={icon.type} />}
/>
)
} else return null
}
export default function LeftMenuButton({ export default function LeftMenuButton({
children, children,
svgIcon,
onClick, onClick,
icon, icon,
disabled = false, disabled = false,
@ -25,7 +48,7 @@ export default function LeftMenuButton({
if (disabled) { if (disabled) {
return ( return (
<div className="left-menu-button link-disabled"> <div className="left-menu-button link-disabled">
<Icon type={icon.type} fw={icon.fw} /> <LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
<span>{children}</span> <span>{children}</span>
{disabledAccesibilityText ? ( {disabledAccesibilityText ? (
<span className="sr-only">{disabledAccesibilityText}</span> <span className="sr-only">{disabledAccesibilityText}</span>
@ -37,7 +60,7 @@ export default function LeftMenuButton({
if (type === 'button') { if (type === 'button') {
return ( return (
<button onClick={onClick} className="left-menu-button"> <button onClick={onClick} className="left-menu-button">
<Icon type={icon.type} fw={icon.fw} /> <LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
<span>{children}</span> <span>{children}</span>
</button> </button>
) )
@ -49,7 +72,7 @@ export default function LeftMenuButton({
rel="noreferrer" rel="noreferrer"
className="left-menu-button" className="left-menu-button"
> >
<Icon type={icon.type} fw={icon.fw} /> <LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
<span>{children}</span> <span>{children}</span>
</a> </a>
) )

View file

@ -1,4 +1,3 @@
import { Form } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import getMeta from '../../../utils/meta' import getMeta from '../../../utils/meta'
import SettingsAutoCloseBrackets from './settings/settings-auto-close-brackets' import SettingsAutoCloseBrackets from './settings/settings-auto-close-brackets'
@ -20,6 +19,7 @@ import SettingsMathPreview from './settings/settings-math-preview'
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 { ElementType } from 'react' import { ElementType } from 'react'
import OLForm from '@/features/ui/components/ol/ol-form'
const moduleSettings: Array<{ const moduleSettings: Array<{
import: { default: ElementType } import: { default: ElementType }
@ -38,7 +38,7 @@ export default function SettingsMenu() {
return ( return (
<> <>
<h4>{t('settings')}</h4> <h4>{t('settings')}</h4>
<Form className="settings"> <OLForm id="left-menu-setting" className="settings">
<SettingsCompiler /> <SettingsCompiler />
<SettingsImageName /> <SettingsImageName />
<SettingsDocument /> <SettingsDocument />
@ -58,7 +58,7 @@ export default function SettingsMenu() {
<SettingsFontFamily /> <SettingsFontFamily />
<SettingsLineHeight /> <SettingsLineHeight />
<SettingsPdfViewer /> <SettingsPdfViewer />
</Form> </OLForm>
</> </>
) )
} }

View file

@ -1,28 +1,31 @@
import { useState } from 'react' import { useState } from 'react'
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import DictionaryModal from '../../../dictionary/components/dictionary-modal' import DictionaryModal from '../../../dictionary/components/dictionary-modal'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
export default function SettingsDictionary() { export default function SettingsDictionary() {
const { t } = useTranslation() const { t } = useTranslation()
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
return ( return (
<div className="form-group left-menu-setting"> <OLFormGroup className="left-menu-setting">
<label htmlFor="dictionary">{t('dictionary')}</label> <OLFormLabel htmlFor="dictionary-settings">{t('dictionary')}</OLFormLabel>
<Button <OLButton
className="btn-secondary" id="dictionary-settings"
bsSize="xs" variant="secondary"
bsStyle={null} size="sm"
onClick={() => setShowModal(true)} onClick={() => setShowModal(true)}
bs3Props={{ bsSize: 'xsmall' }}
> >
{t('edit')} {t('edit')}
</Button> </OLButton>
<DictionaryModal <DictionaryModal
show={showModal} show={showModal}
handleHide={() => setShowModal(false)} handleHide={() => setShowModal(false)}
/> />
</div> </OLFormGroup>
) )
} }

View file

@ -1,4 +1,9 @@
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
import { ChangeEventHandler, useCallback } from 'react' import { ChangeEventHandler, useCallback } from 'react'
import { Spinner } from 'react-bootstrap-5'
type PossibleValue = string | number | boolean type PossibleValue = string | number | boolean
@ -50,16 +55,32 @@ export default function SettingsMenuSelect<T extends PossibleValue = string>({
) )
return ( return (
<div className="form-group left-menu-setting"> <OLFormGroup
<label htmlFor={`settings-menu-${name}`}>{label}</label> controlId={`settings-menu-${name}`}
className="left-menu-setting"
>
<OLFormLabel>{label}</OLFormLabel>
{loading ? ( {loading ? (
<p className="loading pull-right"> <BootstrapVersionSwitcher
<i className="fa fa-fw fa-spin fa-refresh" /> bs3={
</p> <p className="loading pull-right">
<i className="fa fa-fw fa-spin fa-refresh" />
</p>
}
bs5={
<p className="mb-0">
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
</p>
}
/>
) : ( ) : (
<select <OLFormSelect
id={`settings-menu-${name}`} size="sm"
className="form-control"
onChange={handleChange} onChange={handleChange}
value={value?.toString()} value={value?.toString()}
disabled={disabled} disabled={disabled}
@ -86,8 +107,8 @@ export default function SettingsMenuSelect<T extends PossibleValue = string>({
))} ))}
</optgroup> </optgroup>
) : null} ) : null}
</select> </OLFormSelect>
)} )}
</div> </OLFormGroup>
) )
} }

View file

@ -1,8 +1,15 @@
import { Button, Modal, Row, Col } from 'react-bootstrap'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import AccessibleModal from '../../../shared/components/accessible-modal'
import HotkeysModalBottomText from './hotkeys-modal-bottom-text' import HotkeysModalBottomText from './hotkeys-modal-bottom-text'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
export default function HotkeysModal({ export default function HotkeysModal({
animation = true, animation = true,
@ -16,21 +23,16 @@ export default function HotkeysModal({
const ctrl = isMac ? 'Cmd' : 'Ctrl' const ctrl = isMac ? 'Cmd' : 'Ctrl'
return ( return (
<AccessibleModal <OLModal size="lg" onHide={handleHide} show={show} animation={animation}>
bsSize="large" <OLModalHeader closeButton>
onHide={handleHide} <OLModalTitle>{t('hotkeys')}</OLModalTitle>
show={show} </OLModalHeader>
animation={animation}
>
<Modal.Header closeButton>
<Modal.Title>{t('hotkeys')}</Modal.Title>
</Modal.Header>
<Modal.Body className="hotkeys-modal"> <OLModalBody className="hotkeys-modal">
<h3>{t('common')}</h3> <h3>{t('common')}</h3>
<Row> <OLRow>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + F`} combination={`${ctrl} + F`}
description={t('hotkey_find_and_replace')} description={t('hotkey_find_and_replace')}
@ -39,48 +41,48 @@ export default function HotkeysModal({
combination={`${ctrl} + Enter`} combination={`${ctrl} + Enter`}
description={t('hotkey_compile')} description={t('hotkey_compile')}
/> />
</Col> </OLCol>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + Z`} combination={`${ctrl} + Z`}
description={t('hotkey_undo')} description={t('hotkey_undo')}
/> />
</Col> </OLCol>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + Y`} combination={`${ctrl} + Y`}
description={t('hotkey_redo')} description={t('hotkey_redo')}
/> />
</Col> </OLCol>
</Row> </OLRow>
<h3>{t('navigation')}</h3> <h3>{t('navigation')}</h3>
<Row> <OLRow>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + Home`} combination={`${ctrl} + Home`}
description={t('hotkey_beginning_of_document')} description={t('hotkey_beginning_of_document')}
/> />
</Col> </OLCol>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + End`} combination={`${ctrl} + End`}
description={t('hotkey_end_of_document')} description={t('hotkey_end_of_document')}
/> />
</Col> </OLCol>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + Shift + L`} combination={`${ctrl} + Shift + L`}
description={t('hotkey_go_to_line')} description={t('hotkey_go_to_line')}
/> />
</Col> </OLCol>
</Row> </OLRow>
<h3>{t('editing')}</h3> <h3>{t('editing')}</h3>
<Row> <OLRow>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + /`} combination={`${ctrl} + /`}
description={t('hotkey_toggle_comment')} description={t('hotkey_toggle_comment')}
@ -93,9 +95,9 @@ export default function HotkeysModal({
combination={`${ctrl} + A`} combination={`${ctrl} + A`}
description={t('hotkey_select_all')} description={t('hotkey_select_all')}
/> />
</Col> </OLCol>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination="Ctrl + U" combination="Ctrl + U"
description={t('hotkey_to_uppercase')} description={t('hotkey_to_uppercase')}
@ -108,9 +110,9 @@ export default function HotkeysModal({
combination="Tab" combination="Tab"
description={t('hotkey_indent_selection')} description={t('hotkey_indent_selection')}
/> />
</Col> </OLCol>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + B`} combination={`${ctrl} + B`}
description={t('hotkey_bold_text')} description={t('hotkey_bold_text')}
@ -119,31 +121,31 @@ export default function HotkeysModal({
combination={`${ctrl} + I`} combination={`${ctrl} + I`}
description={t('hotkey_italic_text')} description={t('hotkey_italic_text')}
/> />
</Col> </OLCol>
</Row> </OLRow>
<h3>{t('autocomplete')}</h3> <h3>{t('autocomplete')}</h3>
<Row> <OLRow>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination="Ctrl + Space" combination="Ctrl + Space"
description={t('hotkey_autocomplete_menu')} description={t('hotkey_autocomplete_menu')}
/> />
</Col> </OLCol>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination="Up / Down" combination="Up / Down"
description={t('hotkey_select_candidate')} description={t('hotkey_select_candidate')}
/> />
</Col> </OLCol>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination="Enter / Tab" combination="Enter / Tab"
description={t('hotkey_insert_candidate')} description={t('hotkey_insert_candidate')}
/> />
</Col> </OLCol>
</Row> </OLRow>
<h3> <h3>
<Trans <Trans
@ -152,50 +154,50 @@ export default function HotkeysModal({
/> />
</h3> </h3>
<Row> <OLRow>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`Ctrl + Space `} combination={`Ctrl + Space `}
description={t('hotkey_search_references')} description={t('hotkey_search_references')}
/> />
</Col> </OLCol>
</Row> </OLRow>
{trackChangesVisible && ( {trackChangesVisible && (
<> <>
<h3>{t('review')}</h3> <h3>{t('review')}</h3>
<Row> <OLRow>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + J`} combination={`${ctrl} + J`}
description={t('hotkey_toggle_review_panel')} description={t('hotkey_toggle_review_panel')}
/> />
</Col> </OLCol>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + Shift + A`} combination={`${ctrl} + Shift + A`}
description={t('hotkey_toggle_track_changes')} description={t('hotkey_toggle_track_changes')}
/> />
</Col> </OLCol>
<Col xs={4}> <OLCol xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + Shift + C`} combination={`${ctrl} + Shift + C`}
description={t('hotkey_add_a_comment')} description={t('hotkey_add_a_comment')}
/> />
</Col> </OLCol>
</Row> </OLRow>
</> </>
)} )}
<HotkeysModalBottomText /> <HotkeysModalBottomText />
</Modal.Body> </OLModalBody>
<Modal.Footer> <OLModalFooter>
<Button bsStyle={null} className="btn-secondary" onClick={handleHide}> <OLButton variant="secondary" onClick={handleHide}>
{t('close')} {t('close')}
</Button> </OLButton>
</Modal.Footer> </OLModalFooter>
</AccessibleModal> </OLModal>
) )
} }

View file

@ -35,6 +35,7 @@ export function bs3ButtonProps(props: ButtonProps) {
disabled: props.isLoading || props.disabled, disabled: props.isLoading || props.disabled,
form: props.form, form: props.form,
href: props.href, href: props.href,
id: props.id,
target: props.target, target: props.target,
rel: props.rel, rel: props.rel,
onClick: props.onClick, onClick: props.onClick,

View file

@ -9,6 +9,7 @@ export type ButtonProps = {
form?: string form?: string
leadingIcon?: string | React.ReactNode leadingIcon?: string | React.ReactNode
href?: string href?: string
id?: string
target?: string target?: string
rel?: string rel?: string
isLoading?: boolean isLoading?: boolean

View file

@ -12,6 +12,16 @@ export function bsVersion({ bs5, bs3 }: { bs5?: unknown; bs3?: unknown }) {
return isBootstrap5() ? bs5 : bs3 return isBootstrap5() ? bs5 : bs3
} }
export const bsVersionIcon = ({
bs5,
bs3,
}: {
bs5?: { type: string }
bs3?: { type: string; fw?: boolean }
}) => {
return isBootstrap5() ? bs5 : bs3
}
// get all `aria-*` and `data-*` attributes // get all `aria-*` and `data-*` attributes
export const getAriaAndDataProps = (obj: Record<string, unknown>) => { export const getAriaAndDataProps = (obj: Record<string, unknown>) => {
return Object.entries(obj).reduce( return Object.entries(obj).reduce(

View file

@ -1,10 +1,21 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, Button, Modal, Row, Col, Grid } from 'react-bootstrap'
import { useIdeContext } from '../../../shared/context/ide-context' import { useIdeContext } from '../../../shared/context/ide-context'
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '../../../shared/context/project-context'
import { useWordCount } from '../hooks/use-word-count' import { useWordCount } from '../hooks/use-word-count'
import Icon from '../../../shared/components/icon' import {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLNotification from '@/features/ui/components/ol/ol-notification'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
import OLButton from '@/features/ui/components/ol/ol-button'
import Icon from '@/shared/components/icon'
import { Spinner } from 'react-bootstrap-5'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
// NOTE: this component is only mounted when the modal is open // NOTE: this component is only mounted when the modal is open
export default function WordCountModalContent({ handleHide }) { export default function WordCountModalContent({ handleHide }) {
@ -15,71 +26,87 @@ export default function WordCountModalContent({ handleHide }) {
return ( return (
<> <>
<Modal.Header closeButton> <OLModalHeader closeButton>
<Modal.Title>{t('word_count')}</Modal.Title> <OLModalTitle>{t('word_count')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body> <OLModalBody>
{loading && !error && ( {loading && !error && (
<div className="loading"> <div className="loading">
<Icon type="refresh" spin fw /> <BootstrapVersionSwitcher
bs3={<Icon type="refresh" spin fw />}
bs5={
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
}
/>
&nbsp; &nbsp;
{t('loading')} {t('loading')}
</div> </div>
)} )}
{error && ( {error && (
<Alert bsStyle="danger">{t('generic_something_went_wrong')}</Alert> <OLNotification
type="error"
content={t('generic_something_went_wrong')}
/>
)} )}
{data && ( {data && (
<Grid fluid> <div className="container-fluid">
{data.messages && ( {data.messages && (
<Row> <OLRow>
<Col xs={12}> <OLCol xs={12}>
<Alert bsStyle="danger"> <OLNotification
<p style={{ whiteSpace: 'pre-wrap' }}>{data.messages}</p> type="error"
</Alert> content={
</Col> <p style={{ whiteSpace: 'pre-wrap' }}>{data.messages}</p>
</Row> }
/>
</OLCol>
</OLRow>
)} )}
<Row> <OLRow>
<Col xs={4}> <OLCol xs={4}>
<div className="pull-right">{t('total_words')}:</div> <div className="pull-right">{t('total_words')}:</div>
</Col> </OLCol>
<Col xs={6}>{data.textWords}</Col> <OLCol xs={6}>{data.textWords}</OLCol>
</Row> </OLRow>
<Row> <OLRow>
<Col xs={4}> <OLCol xs={4}>
<div className="pull-right">{t('headers')}:</div> <div className="pull-right">{t('headers')}:</div>
</Col> </OLCol>
<Col xs={6}>{data.headers}</Col> <OLCol xs={6}>{data.headers}</OLCol>
</Row> </OLRow>
<Row> <OLRow>
<Col xs={4}> <OLCol xs={4}>
<div className="pull-right">{t('math_inline')}:</div> <div className="pull-right">{t('math_inline')}:</div>
</Col> </OLCol>
<Col xs={6}>{data.mathInline}</Col> <OLCol xs={6}>{data.mathInline}</OLCol>
</Row> </OLRow>
<Row> <OLRow>
<Col xs={4}> <OLCol xs={4}>
<div className="pull-right">{t('math_display')}:</div> <div className="pull-right">{t('math_display')}:</div>
</Col> </OLCol>
<Col xs={6}>{data.mathDisplay}</Col> <OLCol xs={6}>{data.mathDisplay}</OLCol>
</Row> </OLRow>
</Grid> </div>
)} )}
</Modal.Body> </OLModalBody>
<Modal.Footer> <OLModalFooter>
<Button bsStyle={null} className="btn-secondary" onClick={handleHide}> <OLButton variant="secondary" onClick={handleHide}>
{t('close')} {t('close')}
</Button> </OLButton>
</Modal.Footer> </OLModalFooter>
</> </>
) )
} }

View file

@ -1,22 +1,17 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import WordCountModalContent from './word-count-modal-content' import WordCountModalContent from './word-count-modal-content'
import AccessibleModal from '../../../shared/components/accessible-modal'
import withErrorBoundary from '../../../infrastructure/error-boundary' import withErrorBoundary from '../../../infrastructure/error-boundary'
import OLModal from '@/features/ui/components/ol/ol-modal'
const WordCountModal = React.memo(function WordCountModal({ const WordCountModal = React.memo(function WordCountModal({
show, show,
handleHide, handleHide,
}) { }) {
return ( return (
<AccessibleModal <OLModal animation show={show} onHide={handleHide} id="word-count-modal">
animation
show={show}
onHide={handleHide}
id="word-count-modal"
>
<WordCountModalContent handleHide={handleHide} /> <WordCountModalContent handleHide={handleHide} />
</AccessibleModal> </OLModal>
) )
}) })

View file

@ -3,13 +3,18 @@ import Icon from './icon'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { Spinner } from 'react-bootstrap-5' import { Spinner } from 'react-bootstrap-5'
import classNames from 'classnames'
function LoadingSpinner({ function LoadingSpinner({
align,
delay = 0, delay = 0,
loadingText, loadingText,
size,
}: { }: {
align?: 'left' | 'center'
delay?: 0 | 500 // 500 is our standard delay delay?: 0 | 500 // 500 is our standard delay
loadingText?: string loadingText?: string
size?: 'sm'
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -29,6 +34,9 @@ function LoadingSpinner({
return null return null
} }
const alignmentClass =
align === 'left' ? 'align-items-start' : 'align-items-center'
return ( return (
<BootstrapVersionSwitcher <BootstrapVersionSwitcher
bs3={ bs3={
@ -39,12 +47,13 @@ function LoadingSpinner({
</div> </div>
} }
bs5={ bs5={
<div className="text-center mt-4"> <div className={classNames(`d-flex ${alignmentClass}`)}>
<Spinner <Spinner
animation="border" animation="border"
aria-hidden="true" aria-hidden="true"
role="status" role="status"
className="align-middle" className="align-self-center"
size={size}
/> />
&nbsp; &nbsp;
{loadingText || t('loading')} {loadingText || t('loading')}

View file

@ -6,6 +6,7 @@ type IconProps = React.ComponentProps<'i'> & {
type: string type: string
accessibilityLabel?: string accessibilityLabel?: string
modifier?: string modifier?: string
size?: '2x'
} }
function MaterialIcon({ function MaterialIcon({
@ -13,9 +14,12 @@ function MaterialIcon({
className, className,
accessibilityLabel, accessibilityLabel,
modifier, modifier,
size,
...rest ...rest
}: IconProps) { }: IconProps) {
const iconClassName = classNames('material-symbols', className, modifier) const iconClassName = classNames('material-symbols', className, modifier, {
[`size-${size}`]: size,
})
return ( return (
<> <>

View file

@ -0,0 +1,46 @@
function DropboxlLogoBlack() {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_5804_191)">
<path
d="M5.41651 2.16667L0.833374 5.08661L5.41651 8.00655L10.0004 5.08661L5.41651 2.16667Z"
fill="currentColor"
/>
<path
d="M14.5836 2.16667L10.0005 5.08661L14.5836 8.00655L19.1668 5.08661L14.5836 2.16667Z"
fill="currentColor"
/>
<path
d="M0.833374 10.9265L5.41651 13.8464L10.0004 10.9265L5.41651 8.00655L0.833374 10.9265Z"
fill="currentColor"
/>
<path
d="M14.5836 8.00655L10.0005 10.9265L14.5836 13.8464L19.1668 10.9265L14.5836 8.00655Z"
fill="currentColor"
/>
<path
d="M5.4165 14.8198L10.0004 17.7397L14.5836 14.8198L10.0004 11.8998L5.4165 14.8198Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_5804_191">
<rect
width="18.3333"
height="15.573"
fill="white"
transform="translate(0.833374 2.16667)"
/>
</clipPath>
</defs>
</svg>
)
}
export default DropboxlLogoBlack

View file

@ -0,0 +1,30 @@
function GitFork() {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_5804_193)">
<path
d="M18.6495 9.20689L10.795 1.35338C10.3431 0.901045 9.60947 0.901045 9.15698 1.35338L7.52609 2.98443L9.59482 5.05317C10.0758 4.89079 10.627 4.99966 11.0102 5.38292C11.3953 5.76856 11.5034 6.32451 11.337 6.80705L13.3309 8.80094C13.8133 8.63471 14.3698 8.74211 14.755 9.12798C15.2935 9.66633 15.2935 10.5387 14.755 11.0772C14.2164 11.6159 13.3441 11.6159 12.8052 11.0772C12.4002 10.672 12.3001 10.077 12.5052 9.5781L10.6457 7.71858L10.6455 12.6119C10.7768 12.6769 10.9007 12.7636 11.0101 12.8726C11.5485 13.4109 11.5485 14.2831 11.0101 14.8222C10.4716 15.3605 9.59883 15.3605 9.06088 14.8222C8.52245 14.2831 8.52245 13.4109 9.06088 12.8726C9.19395 12.7397 9.34792 12.6392 9.51221 12.5718V7.63301C9.34784 7.56591 9.19405 7.46612 9.06079 7.33222C8.65295 6.9247 8.55472 6.32612 8.76388 5.82529L6.72452 3.78561L1.33936 9.17061C0.88688 9.62329 0.88688 10.3569 1.33936 10.8092L9.19336 18.6628C9.64554 19.1151 10.3789 19.1151 10.8317 18.6628L18.6494 10.8463C19.1017 10.3938 19.1017 9.65999 18.6494 9.20756"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_5804_193">
<rect
width="18"
height="18"
fill="white"
transform="translate(1 1)"
/>
</clipPath>
</defs>
</svg>
)
}
export default GitFork

View file

@ -0,0 +1,48 @@
function GithubLogoBlack() {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.8325 2C5.23119 2 1.5 5.73036 1.5 10.3325C1.5 14.0139 3.88752 17.1373 7.19827 18.239C7.6147 18.3162 7.76759 18.0583 7.76759 17.8382C7.76759 17.6396 7.75986 16.9831 7.75627 16.2869C5.43816 16.7909 4.94902 15.3037 4.94902 15.3037C4.56998 14.3408 4.02384 14.0845 4.02384 14.0845C3.26783 13.5674 4.08083 13.578 4.08083 13.578C4.91756 13.6368 5.35813 14.4368 5.35813 14.4368C6.10131 15.7105 7.30742 15.3422 7.7829 15.1293C7.85783 14.5909 8.07363 14.2232 8.31193 14.0151C6.46117 13.8046 4.51561 13.0899 4.51561 9.89716C4.51561 8.98758 4.84111 8.24413 5.37414 7.6606C5.28763 7.45073 5.00242 6.60324 5.455 5.4555C5.455 5.4555 6.15471 5.23169 7.74689 6.30975C8.41155 6.12513 9.12437 6.03254 9.8325 6.02923C10.5406 6.03254 11.254 6.12513 11.9199 6.30975C13.5103 5.23169 14.209 5.4555 14.209 5.4555C14.6626 6.60324 14.3774 7.45073 14.2909 7.6606C14.8251 8.24413 15.1483 8.98758 15.1483 9.89716C15.1483 13.0977 13.199 13.8022 11.3435 14.0085C11.6424 14.2671 11.9087 14.7742 11.9087 15.5516C11.9087 16.6665 11.8992 17.5638 11.8992 17.8382C11.8992 18.0601 12.0491 18.3198 12.4714 18.2381C15.7805 17.135 18.165 14.0128 18.165 10.3325C18.165 5.73036 14.4344 2 9.8325 2Z"
fill="currentColor"
/>
<path
d="M4.65591 13.9635C4.63756 14.0051 4.57243 14.0175 4.51309 13.9891C4.45252 13.9617 4.41871 13.9053 4.43831 13.8638C4.45625 13.8213 4.52137 13.8094 4.58181 13.8378C4.64225 13.865 4.67674 13.922 4.65591 13.9635Z"
fill="currentColor"
/>
<path
d="M4.99353 14.34C4.95379 14.3769 4.87597 14.3598 4.8234 14.3015C4.76875 14.2434 4.75868 14.1656 4.79897 14.1284C4.83995 14.0915 4.91529 14.1088 4.9698 14.1669C5.02444 14.2256 5.03506 14.3026 4.99353 14.34Z"
fill="currentColor"
/>
<path
d="M5.32207 14.8199C5.27102 14.8555 5.1874 14.8222 5.13579 14.7481C5.08474 14.6739 5.08474 14.585 5.13703 14.5494C5.18864 14.5138 5.27102 14.546 5.32331 14.6194C5.37423 14.6947 5.37423 14.7836 5.32207 14.8199Z"
fill="currentColor"
/>
<path
d="M5.77213 15.2835C5.72646 15.3339 5.62918 15.3204 5.55798 15.2515C5.48512 15.1845 5.46484 15.0891 5.51051 15.0388C5.55688 14.9883 5.65471 15.0025 5.72646 15.0706C5.79876 15.1377 5.8207 15.2337 5.77213 15.2835Z"
fill="currentColor"
/>
<path
d="M6.3931 15.5528C6.37282 15.618 6.27927 15.6475 6.18489 15.6198C6.09064 15.5913 6.02897 15.5149 6.04801 15.449C6.0676 15.3833 6.16171 15.3524 6.25678 15.3821C6.35088 15.4105 6.4127 15.4864 6.3931 15.5528Z"
fill="currentColor"
/>
<path
d="M7.075 15.6026C7.07735 15.6713 6.99732 15.7283 6.89824 15.7296C6.79862 15.7318 6.7179 15.6762 6.7168 15.6086C6.7168 15.5392 6.79517 15.4829 6.89466 15.4811C6.99373 15.4791 7.075 15.5345 7.075 15.6026Z"
fill="currentColor"
/>
<path
d="M7.70952 15.4947C7.72138 15.5616 7.65253 15.6305 7.55415 15.6488C7.45742 15.6666 7.36787 15.6251 7.35559 15.5587C7.34358 15.49 7.41354 15.4211 7.51027 15.4033C7.60879 15.3862 7.69696 15.4265 7.70952 15.4947Z"
fill="currentColor"
/>
</svg>
)
}
export default GithubLogoBlack

View file

@ -247,12 +247,8 @@ $dropdown-item-padding-x: var(--spacing-04);
$dropdown-item-padding-y: var(--spacing-05); $dropdown-item-padding-y: var(--spacing-05);
$dropdown-header-color: var(--content-secondary); $dropdown-header-color: var(--content-secondary);
// List group // Offcanvas
$list-group-color: var(--content-secondary); $offcanvas-horizontal-width: 320px;
$list-group-border-width: 0; $offcanvas-padding-x: 12.5px;
$list-group-border-radius: (--border-radius-base); $offcanvas-padding-y: 12.5px;
$list-group-item-padding-y: var(--spacing-04); $offcanvas-border-width: 0;
$list-group-item-padding-x: var(--spacing-05);
$list-group-hover-bg: var(--bg-light-secondary);
$list-group-disabled-color: var(--content-disabled);
$list-group-disabled-bg: var(--bg-light-primary);

View file

@ -42,6 +42,7 @@
@import 'bootstrap-5/scss/close'; @import 'bootstrap-5/scss/close';
@import 'bootstrap-5/scss/nav'; @import 'bootstrap-5/scss/nav';
@import 'bootstrap-5/scss/navbar'; @import 'bootstrap-5/scss/navbar';
@import 'bootstrap-5/scss/offcanvas';
// Helpers // Helpers
@import 'bootstrap-5/scss/helpers'; @import 'bootstrap-5/scss/helpers';

View file

@ -54,7 +54,3 @@ hr {
.row-spaced-extra-large { .row-spaced-extra-large {
margin-top: calc(var(--line-height-03) * 4); margin-top: calc(var(--line-height-03) * 4);
} }
.rotate-180 {
transform: rotate(180deg);
}

View file

@ -78,6 +78,10 @@ samp {
text-align: center; text-align: center;
} }
.text-muted {
color: var(--content-disabled) !important;
}
@include media-breakpoint-up(lg) { @include media-breakpoint-up(lg) {
.text-center-only-desktop { .text-center-only-desktop {
text-align: center; text-align: center;

View file

@ -24,6 +24,7 @@
@import 'beta-badges'; @import 'beta-badges';
@import 'list-group'; @import 'list-group';
@import 'select'; @import 'select';
@import 'dictionary';
@import 'link'; @import 'link';
@import 'pagination'; @import 'pagination';
@import 'loading-spinner'; @import 'loading-spinner';

View file

@ -0,0 +1,33 @@
#dictionary-modal {
.modal-body {
padding: 0;
}
}
.dictionary-entries-list {
overflow-y: scroll;
max-height: calc(100vh - 225px);
margin: 0;
padding: var(--spacing-04);
}
.dictionary-entry {
word-break: break-all;
display: flex;
padding: var(--spacing-04);
border-bottom: solid 1px var(--border-divider);
align-items: center;
&:last-child {
border-bottom: none;
}
}
.dictionary-entry-name {
flex-grow: 1;
padding-right: var(--spacing-02);
}
.dictionary-empty-body {
padding: var(--spacing-07);
}

View file

@ -6,12 +6,14 @@
@import 'editor/ide'; @import 'editor/ide';
@import 'editor/toolbar'; @import 'editor/toolbar';
@import 'editor/online-users'; @import 'editor/online-users';
@import 'editor/review-panel'; @import 'editor/hotkeys';
@import 'editor/left-menu';
@import 'editor/loading-screen'; @import 'editor/loading-screen';
@import 'editor/outline'; @import 'editor/outline';
@import 'editor/file-tree'; @import 'editor/file-tree';
@import 'editor/file-view'; @import 'editor/file-view';
@import 'editor/figure-modal'; @import 'editor/figure-modal';
@import 'editor/review-panel';
@import 'editor/chat'; @import 'editor/chat';
@import 'subscription'; @import 'subscription';
@import 'editor/pdf'; @import 'editor/pdf';

View file

@ -29,7 +29,7 @@
.sales-contact-form-left-column { .sales-contact-form-left-column {
.sales-contact-form-heading-title { .sales-contact-form-heading-title {
font-size: 2.25rem; font-size: 2.25rem;
font-family: var(-font-family-san-serif); font-family: var(--font-family-san-serif);
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 1.333; line-height: 1.333;

View file

@ -0,0 +1,30 @@
.hotkeys-modal {
font-size: var(--font-size-02);
h3:first-child {
margin-top: 0;
}
.hotkey {
margin-bottom: var(--spacing-05);
}
.description {
display: inline-block;
}
.combination {
padding: var(--spacing-02) var(--spacing-04);
border-radius: 3px;
background-color: var(--bg-dark-tertiary);
color: var(--white);
font-weight: 600;
margin-right: var(--spacing-03);
}
.hotkeys-modal-bottom-text {
background-color: var(--bg-light-secondary);
padding: var(--spacing-04);
border-radius: var(--border-radius-base);
}
}

View file

@ -193,3 +193,31 @@
position: absolute; position: absolute;
inset: 0; inset: 0;
} }
.teaser-title {
margin-top: 0;
text-align: center;
}
.teaser-refresh-label {
text-align: center;
}
.teaser-img {
display: block;
max-width: 100%;
height: auto;
margin-bottom: var(--spacing-03);
}
.teaser-video-container {
margin: calc(var(--spacing-07) * -1) calc(var(--spacing-07) * -1)
var(--spacing-02) calc(var(--spacing-07) * -1);
overflow: hidden;
}
.teaser-video {
width: 100%;
height: auto;
border-bottom: 1px solid var(--border-divider);
}

View file

@ -0,0 +1,247 @@
:root {
--left-menu-form-select-border: var(--border-primary);
}
@include theme('light') {
--left-menu-form-select-border: var(--border-disabled);
}
.left-menu {
position: absolute;
top: 0;
bottom: 0;
background-color: var(--bg-light-secondary);
z-index: 100;
overflow: hidden auto;
transition: left ease-in-out 0.5s;
font-size: var(--font-size-02);
width: 320px;
&.shown {
left: 0;
}
h4 {
font-family: $font-family-sans-serif;
font-weight: 400;
font-size: var(--font-size-03);
margin: var(--spacing-05) 0;
padding-bottom: var(--spacing-03);
color: var(--content-secondary);
border-bottom: 1px solid var(--border-primary-dark);
&:first-child {
margin-top: 0;
}
}
ul.nav {
.left-menu-button {
cursor: pointer;
padding: var(--spacing-03);
font-weight: 700;
color: var(--link-ui);
display: flex;
align-items: center;
width: 100%;
background-color: inherit;
border: none;
text-decoration: none;
.material-symbols {
margin-right: var(--spacing-04);
color: var(--neutral-70);
}
&:hover,
&:active {
background-color: var(--bg-info-01);
color: var(--white);
.material-symbols {
color: var(--white);
}
}
}
a {
cursor: pointer;
&:hover,
&:active,
&:focus {
background-color: var(--bg-info-01);
color: var(--white);
.material-symbols {
color: var(--white);
}
}
.material-symbols {
color: var(--neutral-70);
}
padding: var(--spacing-03);
font-weight: 700;
}
.link-disabled {
color: var(--content-disabled);
}
}
> ul.nav:last-child {
margin-bottom: var(--spacing-05);
}
ul.nav-downloads {
li {
display: inline-block;
text-align: center;
width: 100px;
a {
color: var(--content-secondary);
text-decoration: none;
}
.material-symbols {
margin: var(--spacing-03) 0;
}
}
}
form.settings {
label {
font-weight: normal;
color: var(--content-secondary);
flex: 1 0 50%;
margin-bottom: 0;
padding-right: var(--spacing-03);
white-space: nowrap;
}
button,
select {
width: 50%;
margin: var(--spacing-04) 0;
}
}
.left-menu-setting {
padding: 0 var(--spacing-02);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
border-bottom: 1px solid rgb(0 0 0 / 7%);
margin-bottom: 0;
height: 43px;
&:first-child {
margin-top: calc(var(--spacing-04) * -1);
}
&:last-child {
border-bottom: 0;
}
&:hover {
background-color: var(--bg-info-01);
label {
color: var(--white);
}
}
.form-select {
border: 1px solid var(--left-menu-form-select-border);
}
}
}
#left-menu-mask {
opacity: 0.4;
background-color: #999;
z-index: 99;
transition: opacity 0.5s;
position: absolute;
inset: 0;
}
.left-menu-modal-backdrop {
background-color: transparent;
}
.nav {
margin-bottom: 0;
padding-left: 0;
list-style: none;
display: block;
> li {
position: relative;
display: block;
> a {
position: relative;
display: block;
padding: var(--spacing-04) var(--spacing-06);
&:hover,
&:focus {
text-decoration: none;
background-color: var(--bg-info-01);
color: white;
}
}
// Disabled state sets text to gray and nukes hover/tab effects
&.disabled > a {
color: var(--content-disabled);
&:hover,
&:focus {
color: var(--content-disabled);
text-decoration: none;
background-color: transparent;
cursor: not-allowed;
}
}
}
// Open dropdowns
.open > a {
&,
&:hover,
&:focus {
background-color: var(--bg-info-01);
border-color: var(--link-ui);
}
}
}
.loading-spinner-container {
display: flex;
align-items: center;
justify-content: center;
}
.full-size-loading-spinner-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.reference-manager-search-group {
width: '100%';
border-radius: 0;
text-align: left;
.dropdown-menu {
width: 100%;
}
}

View file

@ -486,7 +486,7 @@ describe('<EditorLeftMenu />', function () {
</EditorProviders> </EditorProviders>
) )
cy.get('label[for="dictionary"] ~ button').click() cy.get('label[for="dictionary-settings"] ~ button').click()
cy.findByText('Edit Dictionary') cy.findByText('Edit Dictionary')
cy.findByText('Your custom dictionary is empty.') cy.findByText('Your custom dictionary is empty.')
}) })

View file

@ -9,7 +9,7 @@ describe('<SettingsDictionary />', function () {
screen.getByText('Dictionary') screen.getByText('Dictionary')
const button = screen.getByRole('button', { name: 'Edit' }) const button = screen.getByText('Edit')
fireEvent.click(button) fireEvent.click(button)
const modal = screen.getAllByRole('dialog')[0] const modal = screen.getAllByRole('dialog')[0]