diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index e94aa7e090..5cc37e7b00 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -1551,7 +1551,9 @@ "took_a_while": "", "toolbar_bullet_list": "", "toolbar_choose_section_heading_level": "", + "toolbar_code_visual_editor_switch": "", "toolbar_decrease_indent": "", + "toolbar_editor": "", "toolbar_format_bold": "", "toolbar_format_italic": "", "toolbar_increase_indent": "", @@ -1562,7 +1564,10 @@ "toolbar_insert_inline_math": "", "toolbar_insert_link": "", "toolbar_insert_math": "", + "toolbar_insert_math_and_symbols": "", + "toolbar_insert_misc": "", "toolbar_insert_table": "", + "toolbar_list_indentation": "", "toolbar_numbered_list": "", "toolbar_redo": "", "toolbar_selected_projects": "", @@ -1571,8 +1576,12 @@ "toolbar_selected_projects_restore": "", "toolbar_table_insert_size_table": "", "toolbar_table_insert_table_lowercase": "", + "toolbar_text_formatting": "", + "toolbar_text_style": "", "toolbar_toggle_symbol_palette": "", "toolbar_undo": "", + "toolbar_undo_redo_actions": "", + "toolbar_visibility": "", "tooltip_hide_filetree": "", "tooltip_hide_pdf": "", "tooltip_show_filetree": "", diff --git a/services/web/frontend/js/features/pdf-preview/components/switch-to-editor-button.tsx b/services/web/frontend/js/features/pdf-preview/components/switch-to-editor-button.tsx index 3c1bf0cf57..ea5d55b3ad 100644 --- a/services/web/frontend/js/features/pdf-preview/components/switch-to-editor-button.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/switch-to-editor-button.tsx @@ -1,6 +1,8 @@ import { useTranslation } from 'react-i18next' -import { Button } from 'react-bootstrap' import Icon from '../../../shared/components/icon' +import MaterialIcon from '@/shared/components/material-icon' +import OLButton from '@/features/ui/components/ol/ol-button' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import { useLayoutContext } from '../../../shared/context/layout-context' function SwitchToEditorButton() { @@ -21,17 +23,21 @@ function SwitchToEditorButton() { } return ( - + } + bs5={} + /> + {t('switch_to_editor')} + ) } diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx index ae595ad17b..24caa9eb7e 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx +++ b/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx @@ -5,7 +5,7 @@ import { useCodeMirrorViewContext, } from './codemirror-context' import { searchPanelOpen } from '@codemirror/search' -import { useResizeObserver } from '../../../shared/hooks/use-resize-observer' +import { useResizeObserver } from '@/shared/hooks/use-resize-observer' import { ToolbarButton } from './toolbar/toolbar-button' import { ToolbarItems } from './toolbar/toolbar-items' import * as commands from '../extensions/toolbar/commands' @@ -21,6 +21,8 @@ import { isVisual } from '../extensions/visual/visual' import { language } from '@codemirror/language' import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors' import { debugConsole } from '@/utils/debugging' +import { bsVersion } from '@/features/utils/bootstrap-5' +import { useTranslation } from 'react-i18next' export const CodeMirrorToolbar = () => { const view = useCodeMirrorViewContext() @@ -34,6 +36,7 @@ export const CodeMirrorToolbar = () => { } const Toolbar = memo(function Toolbar() { + const { t } = useTranslation() const state = useCodeMirrorStateContext() const view = useCodeMirrorViewContext() @@ -140,7 +143,12 @@ const Toolbar = memo(function Toolbar() { const showActions = !state.readOnly && !insideTable return ( -
+
{showActions && (
-
+
diff --git a/services/web/frontend/js/features/source-editor/components/editor-switch.tsx b/services/web/frontend/js/features/source-editor/components/editor-switch.tsx index 531da0d267..c8f53190d2 100644 --- a/services/web/frontend/js/features/source-editor/components/editor-switch.tsx +++ b/services/web/frontend/js/features/source-editor/components/editor-switch.tsx @@ -1,6 +1,6 @@ import { ChangeEvent, FC, memo, useCallback } from 'react' import useScopeValue from '@/shared/hooks/use-scope-value' -import Tooltip from '@/shared/components/tooltip' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' import useTutorial from '@/shared/hooks/promotions/use-tutorial' import { sendMB } from '../../../infrastructure/event-tracking' import isValidTeXFile from '../../../main/is-valid-tex-file' @@ -45,7 +45,10 @@ function EditorSwitch() { ) return ( -
+
Editor mode. @@ -101,14 +104,14 @@ const RichTextToggle: FC<{ if (disabled) { return ( - {toggle} - + ) } diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-body.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-body.tsx index c752dd65a3..4790c40683 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-body.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-body.tsx @@ -1,4 +1,3 @@ -import { Alert } from 'react-bootstrap' import { FigureModalSource, useFigureModalContext, @@ -12,6 +11,7 @@ import { FigureModalCurrentProjectSource } from './file-sources/figure-modal-pro import { FigureModalUploadFileSource } from './file-sources/figure-modal-upload-source' import { FigureModalUrlSource } from './file-sources/figure-modal-url-source' import { useCallback } from 'react' +import OLNotification from '@/features/ui/components/ol/ol-notification' const sourceModes = new Map([ [FigureModalSource.FILE_TREE, FigureModalCurrentProjectSource], @@ -44,9 +44,7 @@ export default function FigureModalBody() { return ( <> {error && ( - - {error} - + )} diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-footer.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-footer.tsx index b1818499a0..43d811c488 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-footer.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-footer.tsx @@ -1,4 +1,3 @@ -import { Button } from 'react-bootstrap' import { FigureModalSource, useFigureModalContext, @@ -7,6 +6,11 @@ import Icon from '../../../../shared/components/icon' import { FC } from 'react' import { useTranslation } from 'react-i18next' import { sendMB } from '../../../../infrastructure/event-tracking' +import OLButton from '@/features/ui/components/ol/ol-button' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import MaterialIcon from '@/shared/components/material-icon' +import { bsVersion } from '@/features/utils/bootstrap-5' +import classnames from 'classnames' export const FigureModalFooter: FC<{ onInsert: () => void @@ -14,23 +18,16 @@ export const FigureModalFooter: FC<{ onDelete: () => void }> = ({ onInsert, onCancel, onDelete }) => { const { t } = useTranslation() + return ( -
-
- -
-
- - -
-
+ <> + + + {t('cancel')} + + + } /> + ) } @@ -39,25 +36,48 @@ const HelpToggle = () => { const { helpShown, dispatch } = useFigureModalContext() if (helpShown) { return ( - + } + bs5={ + + + + } + />{' '} + {t('back')} + ) } return ( - + } + bs5={ + + + + } + />{' '} + {t('help')} + ) } @@ -75,38 +95,29 @@ const FigureModalAction: FC<{ if (sourcePickerShown) { return ( - + ) } if (source === FigureModalSource.EDIT_FIGURE) { return ( - + ) } return ( - + ) } diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-options.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-options.tsx index 2422559bff..31f3b11b78 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-options.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-options.tsx @@ -1,12 +1,18 @@ import { FC } from 'react' -import Icon from '../../../../shared/components/icon' -import Tooltip from '../../../../shared/components/tooltip' +import Icon from '@/shared/components/icon' import { useFigureModalContext, useFigureModalExistingFigureContext, } from './figure-modal-context' -import { Switcher, SwitcherItem } from '../../../../shared/components/switcher' import { useTranslation } from 'react-i18next' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' +import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import OLToggleButtonGroup from '@/features/ui/components/ol/ol-toggle-button-group' +import OLToggleButton from '@/features/ui/components/ol/ol-toggle-button' +import MaterialIcon from '@/shared/components/material-icon' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { bsVersion } from '@/features/utils/bootstrap-5' export const FigureModalFigureOptions: FC = () => { const { t } = useTranslation() @@ -16,71 +22,119 @@ export const FigureModalFigureOptions: FC = () => { const { hasComplexGraphicsArgument } = useFigureModalExistingFigureContext() return ( <> -
- + dispatch({ includeCaption: event.target.checked })} + className={bsVersion({ bs3: 'figure-modal-checkbox-input' })} + label={t('include_caption')} /> - -
-
- + + dispatch({ includeLabel: event.target.checked })} + className={bsVersion({ bs3: 'figure-modal-checkbox-input' })} + label={ + + {t('include_label')} +
+ + {t( + 'used_when_referring_to_the_figure_elsewhere_in_the_document' + )} + +
+ } /> - -
-
-
- {t('image_width')}{' '} - {hasComplexGraphicsArgument ? ( - - - - ) : ( - - - - )} -
-
- + +
+
+ {t('image_width')}{' '} + {hasComplexGraphicsArgument ? ( + + + } + bs5={ + + } + /> + + + ) : ( + + + } + bs5={ + + } + /> + + + )} +
+ dispatch({ width: parseFloat(value) })} defaultValue={width === 1 ? '1.0' : width?.toString()} - disabled={hasComplexGraphicsArgument} + aria-label={t('image_width')} > - - - - - + + {t('1_4_width')} + + + {t('1_2_width')} + + + {t('3_4_width')} + + + {t('full_width')} + +
-
+ ) } diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-source-picker.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-source-picker.tsx index 5bce323471..06af11e461 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-source-picker.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-source-picker.tsx @@ -4,9 +4,11 @@ import { useFigureModalContext, } from './figure-modal-context' import Icon from '../../../../shared/components/icon' -import { Button } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import getMeta from '@/utils/meta' +import MaterialIcon from '@/shared/components/material-icon' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { bsVersion } from '@/features/utils/bootstrap-5' export const FigureModalSourcePicker: FC = () => { const { t } = useTranslation() @@ -16,33 +18,31 @@ export const FigureModalSourcePicker: FC = () => { hasLinkUrlFeature, } = getMeta('ol-ExposedSettings') return ( -
-
+
+ + + {(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && ( + )} + {hasLinkUrlFeature && ( - {(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && ( - - )} - {hasLinkUrlFeature && ( - - )} -
+ )}
) } @@ -54,25 +54,29 @@ const FigureModalSourceButton: FC<{ }> = ({ type, title, icon }) => { const { dispatch } = useFigureModalContext() return ( - + ) } diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal.tsx index 46dc913754..08494d1832 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal.tsx @@ -1,5 +1,9 @@ -import { Modal } from 'react-bootstrap' -import AccessibleModal from '../../../../shared/components/accessible-modal' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' import { FigureModalProvider, FigureModalSource, @@ -268,9 +272,9 @@ const FigureModalContent = () => { return null } return ( - - - + + + {helpShown ? t('help') : sourcePickerShown @@ -281,22 +285,22 @@ const FigureModalContent = () => { url="https://forms.gle/PfEtwceYBNQ32DF4A" text="Please click to give feedback about editing figures." /> - - + + - + }> - + - + - - + + ) } diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/file-name-input.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/file-name-input.tsx index cce93f1d0e..96e5ba987e 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/file-name-input.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/file-name-input.tsx @@ -1,19 +1,16 @@ -import { - DetailedHTMLProps, - InputHTMLAttributes, - useCallback, - useEffect, - useState, -} from 'react' +import { useCallback, useEffect, useState } from 'react' import { File, FileOrDirectory } from '../../utils/file' -import { Alert } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { useCurrentProjectFolders } from '@/features/source-editor/hooks/use-current-project-folders' +import OLFormControl from '@/features/ui/components/ol/ol-form-control' +import OLFormLabel from '@/features/ui/components/ol/ol-form-label' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import OLNotification from '@/features/ui/components/ol/ol-notification' type FileNameInputProps = Omit< - DetailedHTMLProps, HTMLInputElement>, + React.ComponentProps, 'onFocus' -> & { targetFolder: File | null } +> & { targetFolder: File | null; label: string } function findFile( folder: { id: string; name: string }, @@ -52,6 +49,8 @@ function hasOverlap( } export const FileNameInput = ({ + id, + label, targetFolder, ...props }: FileNameInputProps) => { @@ -82,12 +81,19 @@ export const FileNameInput = ({ }, []) return ( <> - - {overlap && ( - - {t('a_file_with_that_name_already_exists_and_will_be_overriden')} - - )} + + {label} + + {overlap && ( + + )} + ) } diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/file-relocator.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/file-relocator.tsx index 8befa99a10..dcc91251d2 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/file-relocator.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/file-relocator.tsx @@ -4,6 +4,7 @@ import { File } from '../../utils/file' import { Select } from '../../../../shared/components/select' import { useCurrentProjectFolders } from '../../hooks/use-current-project-folders' import { useTranslation } from 'react-i18next' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' export const FileRelocator = ({ name, @@ -49,40 +50,36 @@ export const FileRelocator = ({ return ( <> - - { + if (item?.path === '' && item?.name === 'rootFolder') { + return t('no_folder') + } + if (item) { + return `${item.path}${item.name}` + } return t('no_folder') - } - if (item) { - return `${item.path}${item.name}` - } - return t('no_folder') - }} - itemToSubtitle={item => item?.path ?? ''} - itemToKey={item => item.id} - defaultText={t('select_folder_from_project')} - label={t('folder_location')} - optionalLabel - onSelectedItemChanged={selectedFolderChanged} - /> + }} + itemToSubtitle={item => item?.path ?? ''} + itemToKey={item => item.id} + defaultText={t('select_folder_from_project')} + label={t('folder_location')} + optionalLabel + onSelectedItemChanged={selectedFolderChanged} + /> + ) } diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-other-project-source.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-other-project-source.tsx index 3115b2c4dc..6ab6acb6a0 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-other-project-source.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-other-project-source.tsx @@ -13,7 +13,6 @@ import { OutputEntity, useProjectOutputFiles, } from '../../../../file-tree/hooks/use-project-output-files' -import { Button } from 'react-bootstrap' import { useCurrentProjectFolders } from '../../../hooks/use-current-project-folders' import { File, isImageEntity } from '../../../utils/file' import { postJSON } from '../../../../../infrastructure/fetch-json' @@ -23,6 +22,8 @@ import { useTranslation } from 'react-i18next' import { waitForFileTreeUpdate } from '../../../extensions/figure-modal' import { useCodeMirrorViewContext } from '../../codemirror-context' import getMeta from '@/utils/meta' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' function suggestName(path: string) { const parts = path.split('/') @@ -132,54 +133,55 @@ export const FigureModalOtherProjectSource: FC = () => { return ( <> - (project ? project.name : '')} + itemToKey={item => item._id} + defaultText={t('select_a_project_figure_modal')} + label={t('project_figure_modal')} + disabled={projectsLoading} + onSelectedItemChanged={item => { + const suggestion = nameDirty ? name : '' + setName(suggestion) + setSelectedProject(item ?? null) + setFile(null) + updateDispatch({ + newSelectedProject: item ?? null, + newFile: null, + newName: suggestion, + }) + }} + /> + + + { + const suggestion = nameDirty ? name : suggestName(item?.path ?? '') + setName(suggestion) + setFile(item ?? null) + updateDispatch({ + newFile: item ?? null, + newName: suggestion, + }) + }} + /> + {hasLinkedProjectFileFeature && hasLinkedProjectOutputFileFeature && ( +
+ or{' '} + setUsingOutputFiles(value => !value)} + className="p-0 select-from-files-btn" + > {usingOutputFiles ? t('select_from_project_files') : t('select_from_output_files')} - - -
- )} + +
+ )} + { const { t } = useTranslation() @@ -15,27 +16,29 @@ export const FigureModalCurrentProjectSource: FC = () => { const { dispatch, selectedItemId } = useFigureModalContext() const noFiles = files?.length === 0 return ( - (file ? file.name : '')} + itemToSubtitle={item => item?.path ?? ''} + itemToKey={item => item.id} + defaultItem={ + files && selectedItemId + ? files.find(item => item.id === selectedItemId) + : undefined + } + defaultText={ + noFiles + ? t('no_image_files_found') + : t('select_image_from_project_files') + } + label="Image file" + onSelectedItemChanged={item => { + dispatch({ + getPath: item ? async () => `${item.path}${item.name}` : undefined, + }) + }} + /> + ) } diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx index cfc9cb8e18..629c6c79e7 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx @@ -11,12 +11,17 @@ import { refreshProjectMetadata } from '../../../../file-tree/util/api' import { useProjectContext } from '../../../../../shared/context/project-context' import Icon from '../../../../../shared/components/icon' import classNames from 'classnames' -import { Button } from 'react-bootstrap' import { FileRelocator } from '../file-relocator' import { useTranslation } from 'react-i18next' import { useCodeMirrorViewContext } from '../../codemirror-context' import { waitForFileTreeUpdate } from '../../../extensions/figure-modal' import getMeta from '@/utils/meta' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import OLButton from '@/features/ui/components/ol/ol-button' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import MaterialIcon from '@/shared/components/material-icon' +import { Spinner } from 'react-bootstrap-5' +import { isBootstrap5 } from '@/features/utils/bootstrap-5' /* eslint-disable no-unused-vars */ export enum FileUploadStatus { @@ -25,6 +30,7 @@ export enum FileUploadStatus { NOT_ATTEMPTED, UPLOADING, } + /* eslint-enable no-unused-vars */ export const FigureModalUploadFileSource: FC = () => { @@ -199,50 +205,52 @@ export const FigureModalUploadFileSource: FC = () => { return ( <> -
- {file ? ( - { - uppy.removeFile(file.id) - setFile(null) - const newName = nameDirty ? name : '' - setName(newName) - dispatchUploadAction(newName, null, folder) - }} - /> - ) : ( - - )} -
+ +
+ {file ? ( + { + uppy.removeFile(file.id) + setFile(null) + const newName = nameDirty ? name : '' + setName(newName) + dispatchUploadAction(newName, null, folder) + }} + /> + ) : ( + + )} +
+
any }> = ({ name, size, status, onDelete }) => { const { t } = useTranslation() - let icon + let icon = '' switch (status) { case FileUploadStatus.ERROR: - icon = 'times-circle' + icon = isBootstrap5() ? 'cancel' : 'times-circle' break case FileUploadStatus.SUCCESS: - icon = 'check-circle' + icon = isBootstrap5() ? 'check_circle' : 'check-circle' break case FileUploadStatus.NOT_ATTEMPTED: - icon = 'picture-o' + icon = isBootstrap5() ? 'imagesmode' : 'picture-o' break - case FileUploadStatus.UPLOADING: - icon = 'spinner' } + return (
- + {status === FileUploadStatus.UPLOADING ? ( + } + bs5={ +
) @@ -336,8 +361,8 @@ const FileSize: FC<{ size: number; className?: string }> = ({ const [label, bytesPerUnit] = BYTE_UNITS[labelIndex] const sizeInUnits = Math.round(size / bytesPerUnit) return ( - + {sizeInUnits} {label} - + ) } diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-url-source.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-url-source.tsx index 255529287b..2db449e172 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-url-source.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-url-source.tsx @@ -9,6 +9,9 @@ import { useTranslation } from 'react-i18next' import { useCodeMirrorViewContext } from '../../codemirror-context' import { EditorView } from '@codemirror/view' import { waitForFileTreeUpdate } from '../../../extensions/figure-modal' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import OLFormControl from '@/features/ui/components/ol/ol-form-control' +import OLFormLabel from '@/features/ui/components/ol/ol-form-label' function generateLinkedFileFetcher( projectId: string, @@ -72,30 +75,25 @@ export const FigureModalUrlSource: FC = () => { return ( <> - - { - setUrl(e.target.value) - let newName = name - if (!nameDirty) { - // TODO: Improve this - const parts = e.target.value.split('/') - newName = parts[parts.length - 1] ?? '' - setName(newName) - } - ensureButtonActivation(e.target.value, newName, folder) - }} - /> + + {t('image_url')} + { + setUrl(e.target.value) + let newName = name + if (!nameDirty) { + // TODO: Improve this + const parts = e.target.value.split('/') + newName = parts[parts.length - 1] ?? '' + setName(newName) + } + ensureButtonActivation(e.target.value, newName, folder) + }} + /> + - - {t('switch_to_pdf')} - + } + bs5={} + /> + {t('switch_to_pdf')} + ) } diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx index b61dabb421..6f5bd52dbc 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx @@ -1,37 +1,28 @@ import { FC, memo, useRef } from 'react' -import { Button, ListGroup, Overlay, Popover } from 'react-bootstrap' -import Icon from '../../../../shared/components/icon' import useDropdown from '../../../../shared/hooks/use-dropdown' -import Tooltip from '../../../../shared/components/tooltip' +import OLListGroup from '@/features/ui/components/ol/ol-list-group' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' +import OLOverlay from '@/features/ui/components/ol/ol-overlay' +import OLPopover from '@/features/ui/components/ol/ol-popover' import { EditorView } from '@codemirror/view' import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics' import { useCodeMirrorViewContext } from '../codemirror-context' -import MaterialIcon from '../../../../shared/components/material-icon' export const ToolbarButtonMenu: FC<{ id: string label: string - icon: string - materialIcon?: boolean + icon: React.ReactNode altCommand?: (view: EditorView) => void -}> = memo(function ButtonMenu({ - icon, - id, - label, - materialIcon, - altCommand, - children, -}) { +}> = memo(function ButtonMenu({ icon, id, label, altCommand, children }) { const target = useRef(null) const { open, onToggle, ref } = useDropdown() const view = useCodeMirrorViewContext() const button = ( - + {icon} + ) const overlay = ( - onToggle(false)} > - - { onToggle(false) }} > {children} - - - + + + ) if (!label) { @@ -91,14 +82,14 @@ export const ToolbarButtonMenu: FC<{ return ( <> -
} overlayProps={{ placement: 'bottom' }} > {button} - + {overlay} ) diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/insert-figure-dropdown.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/insert-figure-dropdown.tsx index 988fb2cb27..1d29c312df 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/insert-figure-dropdown.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/insert-figure-dropdown.tsx @@ -1,6 +1,8 @@ -import { ListGroupItem } from 'react-bootstrap' import { ToolbarButtonMenu } from './button-menu' -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' +import OLListGroupItem from '@/features/ui/components/ol/ol-list-group-item' import { memo, useCallback } from 'react' import { FigureModalSource } from '../figure-modal/figure-modal-context' import { useTranslation } from 'react-i18next' @@ -32,44 +34,61 @@ export const InsertFigureDropdown = memo(function InsertFigureDropdown() { } + bs5={} + /> + } altCommand={insertFigure} > - openFigureModal(FigureModalSource.FILE_UPLOAD, 'file-upload') } > - + } + bs5={} + /> {t('upload_from_computer')} - - + openFigureModal(FigureModalSource.FILE_TREE, 'current-project') } > - + } + bs5={} + /> {t('from_project_files')} - + {(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && ( - openFigureModal(FigureModalSource.OTHER_PROJECT, 'other-project') } > - + } + bs5={} + /> {t('from_another_project')} - + )} {hasLinkUrlFeature && ( - openFigureModal(FigureModalSource.FROM_URL, 'from-url') } > - + } + bs5={} + /> {t('from_url')} - + )} ) diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx index 45ddf80238..0cead93358 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx @@ -1,4 +1,3 @@ -import { ListGroupItem } from 'react-bootstrap' import { ToolbarButtonMenu } from './button-menu' import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics' import MaterialIcon from '../../../../shared/components/material-icon' @@ -9,6 +8,7 @@ import { wrapInInlineMath, } from '../../extensions/toolbar/commands' import { memo } from 'react' +import OLListGroupItem from '@/features/ui/components/ol/ol-list-group-item' export const MathDropdown = memo(function MathDropdown() { const { t } = useTranslation() @@ -18,10 +18,9 @@ export const MathDropdown = memo(function MathDropdown() { } > - { emitToolbarEvent(view, 'toolbar-inline-math') @@ -32,8 +31,8 @@ export const MathDropdown = memo(function MathDropdown() { > {t('toolbar_insert_inline_math')} - - + { emitToolbarEvent(view, 'toolbar-display-math') @@ -44,7 +43,7 @@ export const MathDropdown = memo(function MathDropdown() { > {t('toolbar_insert_display_math')} - + ) }) diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/overflow.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/overflow.tsx index b9ecee37dc..123f2dc852 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/overflow.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/overflow.tsx @@ -1,21 +1,26 @@ -import { FC, LegacyRef, useRef } from 'react' -import { Button, Overlay, Popover } from 'react-bootstrap' +import { FC, useRef } from 'react' import classnames from 'classnames' import Icon from '../../../../shared/components/icon' +import MaterialIcon from '@/shared/components/material-icon' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import { useCodeMirrorViewContext } from '../codemirror-context' +import OLOverlay from '@/features/ui/components/ol/ol-overlay' +import OLPopover from '@/features/ui/components/ol/ol-popover' +import { bsVersion } from '@/features/utils/bootstrap-5' export const ToolbarOverflow: FC<{ overflowed: boolean overflowOpen: boolean setOverflowOpen: (open: boolean) => void - overflowRef?: LegacyRef + overflowRef?: React.Ref }> = ({ overflowed, overflowOpen, setOverflowOpen, overflowRef, children }) => { - const buttonRef = useRef + } + bs5={} + /> + - setOverflowOpen(false)} > - +
{children}
-
-
+ + ) } diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/section-heading-dropdown.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/section-heading-dropdown.tsx index 36ac0bc815..19c0bf8e52 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/section-heading-dropdown.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/section-heading-dropdown.tsx @@ -8,7 +8,8 @@ import { setSectionHeadingLevel, } from '../../extensions/toolbar/sections' import { useCallback, useMemo, useRef } from 'react' -import { Overlay, Popover } from 'react-bootstrap' +import OLOverlay from '@/features/ui/components/ol/ol-overlay' +import OLPopover from '@/features/ui/components/ol/ol-popover' import useEventListener from '../../../../shared/hooks/use-event-listener' import useDropdown from '../../../../shared/hooks/use-dropdown' import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics' @@ -69,17 +70,27 @@ export const SectionHeadingDropdown = () => { {overflowOpen && ( - setOverflowOpen(false)} - animation={false} + transition={false} container={view.dom} containerPadding={0} placement="bottom" rootClose - target={toggleButtonRef.current ?? undefined} + target={toggleButtonRef.current} + popperConfig={{ + modifiers: [ + { + name: 'offset', + options: { + offset: [0, 1], + }, + }, + ], + }} > - @@ -113,8 +124,8 @@ export const SectionHeadingDropdown = () => { ))}
- - + + )} ) diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/table-inserter-dropdown.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/table-inserter-dropdown.tsx index c5901123b4..1385f9ba98 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/table-inserter-dropdown.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/table-inserter-dropdown.tsx @@ -2,9 +2,10 @@ import { FC, memo, useCallback, useRef, useState } from 'react' import * as commands from '../../extensions/toolbar/commands' import { useTranslation } from 'react-i18next' import useDropdown from '../../../../shared/hooks/use-dropdown' -import { Button, Overlay, Popover } from 'react-bootstrap' import { useCodeMirrorViewContext } from '../codemirror-context' -import Tooltip from '../../../../shared/components/tooltip' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' +import OLOverlay from '@/features/ui/components/ol/ol-overlay' +import OLPopover from '@/features/ui/components/ol/ol-popover' import MaterialIcon from '../../../../shared/components/material-icon' import classNames from 'classnames' import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics' @@ -27,17 +28,16 @@ export const TableInserterDropdown = memo(() => { return ( <> -
} overlayProps={{ placement: 'bottom' }} > - - - + + onToggle(false)} > - {
-
-
+ + ) }) diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-button.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-button.tsx index 3d32ed8652..100f6adf0e 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-button.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-button.tsx @@ -1,11 +1,13 @@ import { memo, useCallback } from 'react' import { EditorView } from '@codemirror/view' import { useCodeMirrorViewContext } from '../codemirror-context' -import { Button } from 'react-bootstrap' import classnames from 'classnames' -import Tooltip from '../../../../shared/components/tooltip' import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics' import Icon from '../../../../shared/components/icon' +import MaterialIcon from '@/shared/components/material-icon' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { bsVersion } from '@/features/utils/bootstrap-5' export const ToolbarButton = memo<{ id: string @@ -49,18 +51,31 @@ export const ToolbarButton = memo<{ ) const button = ( - + {textIcon ? ( + icon + ) : ( + } + bs5={} + /> + )} + ) if (!label) { @@ -75,12 +90,12 @@ export const ToolbarButton = memo<{ ) return ( - {button} - + ) }) diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx index 9ce7f283c7..afaa43c3e1 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx @@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next' import { MathDropdown } from './math-dropdown' import { TableInserterDropdown } from './table-inserter-dropdown' import { withinFormattingCommand } from '@/features/source-editor/utils/tree-operations/formatting' +import { bsVersion } from '@/features/utils/bootstrap-5' const isMac = /Mac/.test(window.navigator?.platform) @@ -37,19 +38,22 @@ export const ToolbarItems: FC<{ return ( <> {showGroup('group-history') && ( -
+
@@ -60,18 +64,22 @@ export const ToolbarItems: FC<{
)} {showGroup('group-format') && ( -
+
@@ -88,6 +98,7 @@ export const ToolbarItems: FC<{
{symbolPaletteAvailable && ( @@ -107,24 +118,25 @@ export const ToolbarItems: FC<{
@@ -134,24 +146,40 @@ export const ToolbarItems: FC<{
@@ -159,7 +187,12 @@ export const ToolbarItems: FC<{ id="toolbar-format-indent-increase" label={t('toolbar_increase_indent')} command={commands.indentIncrease} - icon="indent" + icon={ + bsVersion({ + bs5: 'format_indent_increase', + bs3: 'indent', + }) as string + } shortcut={visual ? (isMac ? '⌘]' : 'Ctrl+]') : undefined} disabled={listDepth < 1} /> diff --git a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts b/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts index 24aa0cb5b0..b62964e474 100644 --- a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts +++ b/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts @@ -43,10 +43,13 @@ const toolbarTheme = EditorView.theme({ borderColor: 'rgba(125, 125, 125, 0.2)', backgroundColor: 'var(--editor-toolbar-bg)', color: 'var(--toolbar-btn-color)', - '& .popover-content': { + '& .popover-content, & .popover-body': { padding: 0, }, - '& .arrow': { + '& .popover-body': { + color: 'inherit', + }, + '& .arrow, & .popover-arrow': { borderBottomColor: 'rgba(125, 125, 125, 0.2)', '&:after': { borderBottomColor: 'var(--editor-toolbar-bg)', @@ -54,10 +57,12 @@ const toolbarTheme = EditorView.theme({ }, }, '.ol-cm-toolbar-button-menu-popover': { - '& > .popover-content': { + backgroundColor: 'initial', + '& > .popover-content, & > .popover-body': { padding: 0, + color: 'initial', }, - '& .arrow': { + '& .arrow, & .popover-arrow': { display: 'none', }, '& .list-group': { @@ -116,6 +121,7 @@ const toolbarTheme = EditorView.theme({ width: '24px', height: '24px', overflow: 'hidden', + color: 'inherit', '&:hover, &:focus, &:active, &.active': { backgroundColor: 'rgba(125, 125, 125, 0.1)', color: 'inherit', @@ -204,11 +210,12 @@ const toolbarTheme = EditorView.theme({ '&.top': { marginBottom: '1px', }, - '& .arrow': { + '& .arrow, & .popover-arrow': { display: 'none', }, - '& .popover-content': { + '& .popover-content, & > .popover-body': { padding: '0', + color: 'inherit', }, '& .ol-cm-toolbar-menu': { width: '120px', @@ -225,6 +232,7 @@ const toolbarTheme = EditorView.theme({ display: 'flex', alignItems: 'center', fontWeight: 'bold', + color: 'inherit', '&.ol-cm-toolbar-menu-item-active': { backgroundColor: 'rgba(125, 125, 125, 0.1)', }, diff --git a/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts b/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts index f5219e1416..8fbe81936c 100644 --- a/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts +++ b/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts @@ -27,7 +27,7 @@ const devToolsButton = ViewPlugin.define(view => { const addButton = () => { const button = document.createElement('button') - button.classList.add('btn', 'formatting-btn', 'formatting-btn--icon') + button.classList.add('btn', 'formatting-btn', 'formatting-btn-icon') button.id = 'cm6-dev-tools-button' button.textContent = '🦧' button.style.border = 'none' diff --git a/services/web/frontend/js/features/ui/components/bootstrap-3/toggle-button-group.tsx b/services/web/frontend/js/features/ui/components/bootstrap-3/toggle-button-group.tsx new file mode 100644 index 0000000000..2224e6160b --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-3/toggle-button-group.tsx @@ -0,0 +1,99 @@ +import { + Children, + cloneElement, + isValidElement, + useState, + useEffect, +} from 'react' +import { + ToggleButtonGroup as BS3ToggleButtonGroup, + ToggleButtonGroupProps as BS3ToggleButtonGroupProps, + ToggleButtonProps as BS3ToggleButtonProps, +} from 'react-bootstrap' + +function ToggleButtonGroup({ + children, + value, + defaultValue, + onChange, + ...props +}: BS3ToggleButtonGroupProps) { + const [selectedValue, setSelectedValue] = useState( + defaultValue || (props.type === 'checkbox' ? [] : null) + ) + const isControlled = value !== undefined + + useEffect(() => { + if (isControlled) { + if (props.type === 'radio') { + setSelectedValue(value) + } else { + if (Array.isArray(value)) { + setSelectedValue(Array.from(value)) + } else { + setSelectedValue([value]) + } + } + } + }, [isControlled, value, props.type]) + + const handleButtonClick = (buttonValue: T) => { + if (props.type === 'radio') { + if (!isControlled) { + setSelectedValue(buttonValue) + } + + onChange?.(buttonValue as any) + } else if (props.type === 'checkbox') { + const newValue = Array.isArray(selectedValue) + ? selectedValue.includes(buttonValue) + ? selectedValue.filter(val => val !== buttonValue) // Deselect + : [...selectedValue, buttonValue] // Select + : [buttonValue] // Initial selection if value is not array yet + + if (!isControlled) { + setSelectedValue(newValue) + } + + onChange?.(newValue) + } + } + + // Clone children and add custom onClick handlers + const modifiedChildren = Children.map(children, child => { + if (isValidElement(child)) { + const childElement = child as React.ReactElement< + BS3ToggleButtonProps & { active?: boolean } + > + + const isActive = + props.type === 'radio' + ? selectedValue === childElement.props.value + : Array.isArray(selectedValue) && + selectedValue.includes(childElement.props.value as T) + + return cloneElement(childElement, { + onClick: () => { + handleButtonClick(childElement.props.value as T) + }, + active: isActive, + }) + } + + return child + }) + + return ( + {}} + > + {modifiedChildren} + + ) +} + +export default ToggleButtonGroup diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx index 584dae9485..0ba364a19e 100644 --- a/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx @@ -17,6 +17,7 @@ import type { } from '@/features/ui/components/types/dropdown-menu-props' import MaterialIcon from '@/shared/components/material-icon' import { fixedForwardRef } from '@/utils/react' +import classnames from 'classnames' export function Dropdown({ ...props }: DropdownProps) { return @@ -26,6 +27,7 @@ function DropdownItem( { active, children, + className, description, leadingIcon, trailingIcon, @@ -74,7 +76,9 @@ function DropdownItem( return ( size === 'small' ? 'sm' : size === 'large' ? 'lg' : undefined -export default function OLButton(props: OLButtonProps) { - const { bs3Props, ...rest } = props +export default function OLButton({ bs3Props = {}, ...rest }: OLButtonProps) { + const { className: _, ...restBs3Props } = bs3Props // Get all `aria-*` and `data-*` attributes const extraProps = getAriaAndDataProps(rest) @@ -52,7 +53,15 @@ export default function OLButton(props: OLButtonProps) { return ( + {bs3Props?.loading || rest.children} } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx index 971e043782..e89ea75b34 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx @@ -20,6 +20,8 @@ function OLFormCheckbox(props: OLFormCheckboxProps) { inline: rest.inline, title: rest.title, autoComplete: rest.autoComplete, + defaultChecked: rest.defaultChecked, + className: rest.className, onChange: rest.onChange as (e: React.ChangeEvent) => void, inputRef: node => { if (inputRef) { diff --git a/services/web/frontend/js/features/ui/components/ol/ol-list-group-item.tsx b/services/web/frontend/js/features/ui/components/ol/ol-list-group-item.tsx new file mode 100644 index 0000000000..c177ee3a1c --- /dev/null +++ b/services/web/frontend/js/features/ui/components/ol/ol-list-group-item.tsx @@ -0,0 +1,42 @@ +import { ListGroupItem, ListGroupItemProps } from 'react-bootstrap-5' +import { + ListGroupItem as BS3ListGroupItem, + ListGroupItemProps as BS3ListGroupItemProps, +} from 'react-bootstrap' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' + +type OLListGroupItemProps = ListGroupItemProps & { + bs3Props?: BS3ListGroupItemProps +} + +function OLListGroupItem(props: OLListGroupItemProps) { + const { bs3Props, ...rest } = props + + const bs3ListGroupItemProps: BS3ListGroupItemProps = { + children: rest.children, + active: rest.active, + disabled: rest.disabled, + href: rest.href, + onClick: rest.onClick as BS3ListGroupItemProps['onClick'], + ...bs3Props, + } + + const extraProps = getAriaAndDataProps(rest) + const as = rest.as ?? 'button' + + return ( + } + bs5={ + + } + /> + ) +} + +export default OLListGroupItem diff --git a/services/web/frontend/js/features/ui/components/ol/ol-list-group.tsx b/services/web/frontend/js/features/ui/components/ol/ol-list-group.tsx new file mode 100644 index 0000000000..65af7b3ec3 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/ol/ol-list-group.tsx @@ -0,0 +1,34 @@ +import { ListGroup, ListGroupProps } from 'react-bootstrap-5' +import { + ListGroup as BS3ListGroup, + ListGroupProps as BS3ListGroupProps, +} from 'react-bootstrap' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' + +type OLListGroupProps = ListGroupProps & { + bs3Props?: BS3ListGroupProps +} + +function OLListGroup(props: OLListGroupProps) { + const { bs3Props, ...rest } = props + + const bs3ListGroupProps: BS3ListGroupProps = { + children: rest.children, + role: rest.role, + componentClass: rest.as, + ...bs3Props, + } + + const extraProps = getAriaAndDataProps(rest) + const as = rest.as ?? 'div' + + return ( + } + bs5={} + /> + ) +} + +export default OLListGroup diff --git a/services/web/frontend/js/features/ui/components/ol/ol-overlay.tsx b/services/web/frontend/js/features/ui/components/ol/ol-overlay.tsx new file mode 100644 index 0000000000..3fa2d3e34a --- /dev/null +++ b/services/web/frontend/js/features/ui/components/ol/ol-overlay.tsx @@ -0,0 +1,60 @@ +import { Overlay, OverlayProps } from 'react-bootstrap-5' +import { + Overlay as BS3Overlay, + OverlayProps as BS3OverlayProps, +} from 'react-bootstrap' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' + +type OLOverlayProps = OverlayProps & { + bs3Props?: BS3OverlayProps +} + +function OLOverlay(props: OLOverlayProps) { + const { bs3Props, ...bs5Props } = props + + let bs3OverlayProps: BS3OverlayProps = { + children: bs5Props.children, + target: bs5Props.target as BS3OverlayProps['target'], + container: bs5Props.container, + containerPadding: bs5Props.containerPadding, + show: bs5Props.show, + rootClose: bs5Props.rootClose, + animation: bs5Props.transition, + onHide: bs5Props.onHide as BS3OverlayProps['onHide'], + onEnter: bs5Props.onEnter as BS3OverlayProps['onEnter'], + onEntering: bs5Props.onEntering as BS3OverlayProps['onEntering'], + onEntered: bs5Props.onEntered as BS3OverlayProps['onEntered'], + onExit: bs5Props.onExit as BS3OverlayProps['onExit'], + onExiting: bs5Props.onExiting as BS3OverlayProps['onExiting'], + onExited: bs5Props.onExited as BS3OverlayProps['onExited'], + } + + if (bs5Props.placement) { + const bs3PlacementOptions = [ + 'top', + 'right', + 'bottom', + 'left', + ] satisfies Array< + Extract + > + + for (const placement of bs3PlacementOptions) { + if (placement === bs5Props.placement) { + bs3OverlayProps.placement = bs5Props.placement + break + } + } + } + + bs3OverlayProps = { ...bs3OverlayProps, ...bs3Props } + + return ( + } + bs5={} + /> + ) +} + +export default OLOverlay diff --git a/services/web/frontend/js/features/ui/components/ol/ol-popover.tsx b/services/web/frontend/js/features/ui/components/ol/ol-popover.tsx new file mode 100644 index 0000000000..7e6d1feeca --- /dev/null +++ b/services/web/frontend/js/features/ui/components/ol/ol-popover.tsx @@ -0,0 +1,85 @@ +import { forwardRef } from 'react' +import { Popover, PopoverProps } from 'react-bootstrap-5' +import { + Popover as BS3Popover, + PopoverProps as BS3PopoverProps, +} from 'react-bootstrap' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' + +type OLPopoverProps = PopoverProps & { + title?: React.ReactNode + bs3Props?: BS3PopoverProps +} + +const OLPopover = forwardRef((props, ref) => { + // BS3 passes in some props automatically so the `props` + // type should be adjusted to reflect the actual received object + const propsCombinedWithAutoInjectedBs3Props = props as OLPopoverProps & + Pick< + BS3PopoverProps, + 'arrowOffsetLeft' | 'arrowOffsetTop' | 'positionLeft' | 'positionTop' + > + + const { + bs3Props, + title, + children, + arrowOffsetLeft, + arrowOffsetTop, + positionLeft, + positionTop, + ...bs5Props + } = propsCombinedWithAutoInjectedBs3Props + + let bs3PopoverProps: BS3PopoverProps = { + children, + arrowOffsetLeft, + arrowOffsetTop, + positionLeft, + positionTop, + title, + id: bs5Props.id, + className: bs5Props.className, + style: bs5Props.style, + } + + if (bs5Props.placement) { + const bs3PlacementOptions = [ + 'top', + 'right', + 'bottom', + 'left', + ] satisfies Array< + Extract + > + + for (const placement of bs3PlacementOptions) { + if (placement === bs5Props.placement) { + bs3PopoverProps.placement = bs5Props.placement + break + } + } + } + + bs3PopoverProps = { ...bs3PopoverProps, ...bs3Props } + + return ( + } + /> + } + bs5={ + + {title && {title}} + {children} + + } + /> + ) +}) +OLPopover.displayName = 'OLPopover' + +export default OLPopover diff --git a/services/web/frontend/js/features/ui/components/ol/ol-toggle-button-group.tsx b/services/web/frontend/js/features/ui/components/ol/ol-toggle-button-group.tsx new file mode 100644 index 0000000000..ea874c48a7 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/ol/ol-toggle-button-group.tsx @@ -0,0 +1,42 @@ +import { ToggleButtonGroup, ToggleButtonGroupProps } from 'react-bootstrap-5' +import BS3ToggleButtonGroup from '@/features/ui/components/bootstrap-3/toggle-button-group' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' + +type BS3ToggleButtonGroupProps = React.ComponentProps< + typeof BS3ToggleButtonGroup +> + +type OLToggleButtonGroupProps = ToggleButtonGroupProps & { + bs3Props?: BS3ToggleButtonGroupProps +} + +function OLToggleButtonGroup(props: OLToggleButtonGroupProps) { + const { bs3Props, ...rest } = props + + const bs3ToggleButtonGroupProps = { + name: rest.name, + type: rest.type, + value: rest.value, + onChange: rest.onChange, + children: rest.children, + className: rest.className, + defaultValue: rest.defaultValue, + defaultChecked: rest.defaultChecked, + ...bs3Props, + } as BS3ToggleButtonGroupProps + + // Get all `aria-*` and `data-*` attributes + const extraProps = getAriaAndDataProps(rest) + + return ( + + } + bs5={} + /> + ) +} + +export default OLToggleButtonGroup diff --git a/services/web/frontend/js/features/ui/components/ol/ol-toggle-button.tsx b/services/web/frontend/js/features/ui/components/ol/ol-toggle-button.tsx new file mode 100644 index 0000000000..4415e71bbe --- /dev/null +++ b/services/web/frontend/js/features/ui/components/ol/ol-toggle-button.tsx @@ -0,0 +1,48 @@ +import { ToggleButton, ToggleButtonProps } from 'react-bootstrap-5' +import { + ToggleButton as BS3ToggleButton, + ToggleButtonProps as BS3ToggleButtonProps, +} from 'react-bootstrap' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' +import classnames from 'classnames' + +type OLToggleButtonProps = ToggleButtonProps & { + bs3Props?: BS3ToggleButtonProps +} + +function OLToggleButton(props: OLToggleButtonProps) { + const { bs3Props, ...rest } = props + + const bs3ToggleButtonProps: BS3ToggleButtonProps & { active?: boolean } = { + type: rest.type, + name: rest.name, + active: rest.active, + checked: rest.checked, + disabled: rest.disabled, + onChange: rest.onChange as BS3ToggleButtonProps['onChange'], + onClick: rest.onClick as BS3ToggleButtonProps['onClick'], + value: rest.value as BS3ToggleButtonProps['value'], + children: rest.children, + className: classnames(`btn-${props.variant || 'primary'}`, rest.className), + ...bs3Props, + } + + // Get all `aria-*` and `data-*` attributes + const extraProps = getAriaAndDataProps(rest) + + return ( + + } + bs5={} + /> + ) +} + +export default OLToggleButton diff --git a/services/web/frontend/js/shared/components/beta-badge.tsx b/services/web/frontend/js/shared/components/beta-badge.tsx index 80e5eaec46..907d53d1e7 100644 --- a/services/web/frontend/js/shared/components/beta-badge.tsx +++ b/services/web/frontend/js/shared/components/beta-badge.tsx @@ -1,13 +1,16 @@ import type { FC, ReactNode } from 'react' import classnames from 'classnames' -import Tooltip from './tooltip' -import { OverlayTriggerProps } from 'react-bootstrap' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' +import MaterialIcon from '@/shared/components/material-icon' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' type TooltipProps = { id: string text: ReactNode - placement?: OverlayTriggerProps['placement'] className?: string + placement?: NonNullable< + React.ComponentProps['overlayProps'] + >['placement'] } const BetaBadge: FC<{ @@ -15,7 +18,7 @@ const BetaBadge: FC<{ url?: string phase?: string }> = ({ tooltip, url = '/beta/participate', phase = 'beta' }) => { - let badgeClass + let badgeClass: 'info-badge' | 'alpha-badge' | 'beta-badge' switch (phase) { case 'release': badgeClass = 'info-badge' @@ -29,24 +32,28 @@ const BetaBadge: FC<{ } return ( - - + {tooltip.text} + } + bs5={ + + } + /> - + ) } diff --git a/services/web/frontend/js/shared/components/select.tsx b/services/web/frontend/js/shared/components/select.tsx index f657d2cda9..addb605194 100644 --- a/services/web/frontend/js/shared/components/select.tsx +++ b/services/web/frontend/js/shared/components/select.tsx @@ -12,6 +12,11 @@ import classNames from 'classnames' import { useSelect } from 'downshift' import Icon from './icon' import { useTranslation } from 'react-i18next' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { Form, Spinner } from 'react-bootstrap-5' +import FormControl from '@/features/ui/components/bootstrap-5/form/form-control' +import MaterialIcon from '@/shared/components/material-icon' +import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu' export type SelectProps = { // The items rendered as dropdown options. @@ -79,6 +84,7 @@ export const Select = ({ getItemProps, highlightedIndex, openMenu, + closeMenu, } = useSelect({ items: items ?? [], itemToString, @@ -117,9 +123,16 @@ export const Select = ({ } }, [name, itemToString, selectedItem, defaultItem]) + const handleMenuKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape' && isOpen) { + event.stopPropagation() + closeMenu() + } + } + const onKeyDown: KeyboardEventHandler = useCallback( event => { - if (event.key === 'Enter' && !isOpen) { + if ((event.key === 'Enter' || event.key === ' ') && !isOpen) { event.preventDefault() ;(event.nativeEvent as any).preventDownshiftDefault = true openMenu() @@ -135,77 +148,148 @@ export const Select = ({ value = defaultText } return ( -
-
- {label ? ( - - ) : null} -
-
{value}
+
- {isOpen ? ( - - ) : ( - - )} -
-
-
-
    - {isOpen && - items?.map((item, index) => { - const isDisabled = itemToDisabled && itemToDisabled(item) - return ( -
  • - - {selectedIcon && ( -
    - {(selectedItem === item || - (!selectedItem && defaultItem === item)) && ( - - )} -
    - )} - {itemToString(item)} -
    - - {itemToSubtitle ? ( - - {itemToSubtitle(item)} + {label ? ( +
  • - ) - })} -
-
+ )}{' '} + {loading && ( + + )} + + ) : null} +
+
{value}
+
+ {isOpen ? ( + + ) : ( + + )} +
+
+
+
    + {isOpen && + items?.map((item, index) => { + const isDisabled = itemToDisabled && itemToDisabled(item) + return ( +
  • + + {selectedIcon && ( +
    + {(selectedItem === item || + (!selectedItem && defaultItem === item)) && ( + + )} +
    + )} + {itemToString(item)} +
    + + {itemToSubtitle ? ( + + {itemToSubtitle(item)} + + ) : null} +
  • + ) + })} +
+
+ } + bs5={ +
+ {label ? ( + + {label}{' '} + {optionalLabel && ( + ({t('optional')}) + )}{' '} + {loading && ( + + + )} + + ) : null} + + } + /> +
    + {isOpen && + items?.map((item, index) => { + const isDisabled = itemToDisabled && itemToDisabled(item) + return ( +
  • + + {itemToString(item)} + +
  • + ) + })} +
+
+ } + /> ) } diff --git a/services/web/frontend/js/shared/components/switcher.tsx b/services/web/frontend/js/shared/components/switcher.tsx deleted file mode 100644 index e75e6bc4b8..0000000000 --- a/services/web/frontend/js/shared/components/switcher.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { FC, createContext, useContext } from 'react' - -const SwitcherContext = createContext< - | { - name: string - onChange?: (value: string) => any - defaultValue?: string - disabled: boolean - } - | undefined ->(undefined) - -export const Switcher: FC<{ - name: string - onChange?: (value: string) => any - defaultValue?: string - disabled?: boolean -}> = ({ name, children, onChange, defaultValue, disabled = false }) => { - return ( - -
{children}
-
- ) -} - -export const SwitcherItem: FC<{ - value: string - label: string - checked?: boolean -}> = ({ value, label, checked = false }) => { - const ctx = useContext(SwitcherContext) - if (!ctx) { - throw new Error('SwitcherItem must be a child of Switcher') - } - const { name, onChange, defaultValue, disabled } = ctx - - const id = `${name}-option-${value.replace(/\W/g, '')}` - return ( - <> - { - if (onChange) { - onChange(evt.target.value) - } - }} - /> - - - ) -} diff --git a/services/web/frontend/stories/switcher.stories.tsx b/services/web/frontend/stories/switcher.stories.tsx index c713aaf025..da7002ae26 100644 --- a/services/web/frontend/stories/switcher.stories.tsx +++ b/services/web/frontend/stories/switcher.stories.tsx @@ -1,28 +1,55 @@ -import { Switcher, SwitcherItem } from '../js/shared/components/switcher' +import OLToggleButton from '@/features/ui/components/ol/ol-toggle-button' +import OLToggleButtonGroup from '@/features/ui/components/ol/ol-toggle-button-group' export const Base = () => { return ( - - - - - - + + + ¼ width + + + ½ width + + + ¾ width + + + Full width + + ) } export const Disabled = () => { return ( - - - - - - + + + ¼ width + + + ½ width + + + ¾ width + + + Full width + + ) } export default { - title: 'Shared / Components / Switcher', - component: Switcher, + title: 'Shared / Components / Toggle Button Group', + component: OLToggleButtonGroup, } diff --git a/services/web/frontend/stylesheets/app/editor/figure-modal.less b/services/web/frontend/stylesheets/app/editor/figure-modal.less index ab31a3365d..52157ec15e 100644 --- a/services/web/frontend/stylesheets/app/editor/figure-modal.less +++ b/services/web/frontend/stylesheets/app/editor/figure-modal.less @@ -1,27 +1,3 @@ -.figure-modal-label { - font-weight: normal; - line-height: 100%; -} - -.figure-modal-form tr, -.figure-modal-form td { - vertical-align: top; -} - -.figure-modal-footer { - display: flex; - justify-content: space-between; -} - -.figure-modal-help-buttons, -.figure-modal-actions { - flex: 1 1 auto; -} - -.figure-modal-help-buttons { - text-align: left; -} - .figure-modal-help-link, .figure-modal-help-link:hover, .figure-modal-help-link:focus, @@ -34,29 +10,32 @@ padding-left: 0; } -.figure-modal-switcher-input { - display: flex; -} +.figure-modal-checkbox-input { + label { + display: inline-block; + font-weight: normal; + line-height: 100%; -.figure-modal-checkbox-input, -.figure-modal-switcher-input { - margin-top: 16px; -} + .figure-modal-label-content { + display: block; + overflow: hidden; + } + } -.figure-modal-checkbox-input input[type='checkbox'] { - vertical-align: top; - margin-right: 12px; - accent-color: @ol-green; + input[type='checkbox'] { + margin-right: 12px; + margin-top: 2px; + vertical-align: top; + accent-color: @ol-green; + } } .figure-modal-switcher-input { + position: relative; + z-index: 0; display: flex; justify-content: space-between; - vertical-align: middle; -} - -.figure-modal-input-field { - width: 100%; + align-items: center; } .file-container { @@ -71,6 +50,7 @@ .file-container-file { display: flex; + align-items: center; border: 1px solid @neutral-20; border-radius: 8px; background-color: white; @@ -92,7 +72,6 @@ .file-icon { font-size: 20px; - line-height: 40px; } .file-action { @@ -129,17 +108,13 @@ } .figure-modal-source-button-icon { - flex: 0 0 auto; - &.source-icon { - margin-right: 6px; - } -} - -.figure-modal-input-label:not(:first-child), -.figure-modal .select-wrapper:not(:first-child) { - margin-top: 16px; + margin-right: 6px; } .figure-modal-upload .uppy-Dashboard-AddFiles-list { display: none; } + +.select-from-files-btn { + vertical-align: baseline; +} diff --git a/services/web/frontend/stylesheets/app/editor/toolbar.less b/services/web/frontend/stylesheets/app/editor/toolbar.less index d215966aba..4fd7c1b954 100644 --- a/services/web/frontend/stylesheets/app/editor/toolbar.less +++ b/services/web/frontend/stylesheets/app/editor/toolbar.less @@ -387,12 +387,12 @@ } } -.formatting-btn--icon { +.formatting-btn-icon { min-width: 32px; width: 32px; } -.formatting-btn--icon:last-of-type { +.formatting-btn-icon:last-of-type { border-right: 1px solid @formatting-btn-border; } diff --git a/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss b/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss index 39277ca24b..b92f7f7411 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss @@ -175,6 +175,24 @@ $tooltip-border-radius: $border-radius-base; $tooltip-padding-y: $spacing-04; $tooltip-padding-x: $spacing-06; +// Popovers +$popover-font-size: var(--font-size-02); +$popover-bg: var(--bg-dark-primary); +$popover-max-width: 320px; +$popover-border-width: 0px; /* stylelint-disable-line length-zero-no-unit */ +$popover-border-radius: var(--border-radius-base); +$popover-header-font-size: var(--font-size-02); +$popover-header-bg: var(--bg-dark-primary); +$popover-header-color: var(--content-primary-dark); +$popover-header-padding-y: var(--spacing-04); +$popover-header-padding-x: var(--spacing-06); +$popover-body-color: var(--content-primary-dark); +$popover-body-padding-y: var(--spacing-04); +$popover-body-padding-x: var(--spacing-06); +$popover-arrow-width: var(--spacing-04); +$popover-arrow-height: var(--spacing-02); +$popover-arrow-color: var(--bg-dark-primary); + // Links. Ideally we'd point these to CSS variables but Bootstrap performs // calculations on link color during compilation. $link-color: $link-ui; @@ -228,3 +246,13 @@ $dropdown-padding-y: var(--spacing-02); $dropdown-item-padding-x: var(--spacing-04); $dropdown-item-padding-y: var(--spacing-05); $dropdown-header-color: var(--content-secondary); + +// List group +$list-group-color: var(--content-secondary); +$list-group-border-width: 0; +$list-group-border-radius: (--border-radius-base); +$list-group-item-padding-y: var(--spacing-04); +$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); diff --git a/services/web/frontend/stylesheets/bootstrap-5/base/bootstrap.scss b/services/web/frontend/stylesheets/bootstrap-5/base/bootstrap.scss index b486476f67..16d68fb7c3 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/base/bootstrap.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/base/bootstrap.scss @@ -33,8 +33,10 @@ @import 'bootstrap-5/scss/dropdown'; @import 'bootstrap-5/scss/button-group'; @import 'bootstrap-5/scss/badge'; +@import 'bootstrap-5/scss/list-group'; @import 'bootstrap-5/scss/modal'; @import 'bootstrap-5/scss/tooltip'; +@import 'bootstrap-5/scss/popover'; @import 'bootstrap-5/scss/spinners'; @import 'bootstrap-5/scss/card'; @import 'bootstrap-5/scss/close'; diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/all.scss b/services/web/frontend/stylesheets/bootstrap-5/components/all.scss index 7d50253664..523b901bc5 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/all.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/all.scss @@ -6,6 +6,7 @@ @import 'notifications'; @import 'system-messages'; @import 'tooltip'; +@import 'popover'; @import 'card'; @import 'badge'; @import 'form'; @@ -20,3 +21,6 @@ @import 'table'; @import 'blog-posts'; @import 'tabs'; +@import 'beta-badges'; +@import 'list-group'; +@import 'select'; diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/beta-badges.scss b/services/web/frontend/stylesheets/bootstrap-5/components/beta-badges.scss new file mode 100644 index 0000000000..e35528af78 --- /dev/null +++ b/services/web/frontend/stylesheets/bootstrap-5/components/beta-badges.scss @@ -0,0 +1,11 @@ +.info-badge { + color: var(--blue-50); +} + +.alpha-badge { + color: var(--green-50); +} + +.beta-badge { + color: var(--yellow-40); +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/form.scss b/services/web/frontend/stylesheets/bootstrap-5/components/form.scss index 1c1a34953d..b68035a672 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/form.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/form.scss @@ -84,7 +84,7 @@ } .form-label { - &:has(+ .form-control[disabled]) { + &:has(+ .form-control[disabled], + .form-control-wrapper-disabled) { color: var(--content-disabled); } } diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/list-group.scss b/services/web/frontend/stylesheets/bootstrap-5/components/list-group.scss new file mode 100644 index 0000000000..d3bb6db123 --- /dev/null +++ b/services/web/frontend/stylesheets/bootstrap-5/components/list-group.scss @@ -0,0 +1,3 @@ +.list-group-item { + min-height: 48px; +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/popover.scss b/services/web/frontend/stylesheets/bootstrap-5/components/popover.scss new file mode 100644 index 0000000000..d1bacede62 --- /dev/null +++ b/services/web/frontend/stylesheets/bootstrap-5/components/popover.scss @@ -0,0 +1,15 @@ +.popover { + @include shadow-md; + + line-height: var(--line-height-02); +} + +.popover-header { + padding-bottom: 0; + margin-bottom: var(--spacing-02); + font-weight: 600; + + & + .popover-body { + padding-top: 0; + } +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/select.scss b/services/web/frontend/stylesheets/bootstrap-5/components/select.scss new file mode 100644 index 0000000000..939397bc86 --- /dev/null +++ b/services/web/frontend/stylesheets/bootstrap-5/components/select.scss @@ -0,0 +1,11 @@ +.select-wrapper { + position: relative; +} + +.select-trigger { + cursor: default; +} + +.select-highlighted { + background-color: var(--bg-light-secondary); +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss index 776ea697ff..aad6f1471a 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss @@ -10,4 +10,5 @@ @import 'editor/loading-screen'; @import 'editor/outline'; @import 'editor/file-tree'; +@import 'editor/figure-modal'; @import 'website-redesign'; diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/figure-modal.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/figure-modal.scss new file mode 100644 index 0000000000..f071304888 --- /dev/null +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/figure-modal.scss @@ -0,0 +1,89 @@ +.figure-modal-help-link, +.figure-modal-help-link:hover, +.figure-modal-help-link:focus, +.figure-modal-help-link:active { + color: var(--neutral-90) !important; + text-decoration: none; +} + +.figure-modal-help-link { + padding-left: 0; +} + +.figure-modal-switcher-input { + position: relative; + z-index: 0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.file-container { + width: 100%; + height: 120px; + padding: var(--spacing-07) var(--spacing-08); + border: 1px dashed #eaeaea; + background-color: #fafafa; + justify-content: space-between; + border-radius: var(--border-radius-medium); +} + +.file-container-file { + display: flex; + align-items: center; + border: 1px solid var(--neutral-20); + border-radius: var(--border-radius-medium); + background-color: white; + height: 100%; + padding: var(--spacing-06) var(--spacing-07); +} + +.file-info { + margin-left: var(--spacing-07); + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: space-around; +} + +.file-name { + font-weight: bold; +} + +.figure-modal-source-button-grid { + display: grid; + justify-content: space-between; + gap: 8px; + grid-template-columns: 1fr 1fr; + margin: 0 auto; + + &:not(:first-of-type) { + margin-top: 8px; + } +} + +.figure-modal-source-button { + display: flex; + flex: 1 1 0; + align-items: center; + box-shadow: 0 2px 4px 0 #1e253029; + line-height: 44px; + background-color: var(--white); + border-radius: var(--border-radius-base); + border: none; + padding: 0 var(--spacing-04); + + &-title { + flex: 1 1 auto; + text-align: left; + } +} + +.figure-modal-source-button-icon { + margin-right: var(--spacing-03); +} + +/* stylelint-disable selector-class-pattern */ +.figure-modal-upload .uppy-Dashboard-AddFiles-list { + display: none; +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss index 16801b9292..966593f704 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss @@ -79,7 +79,7 @@ } .ide-panel-group-resizing { - background-color: white; + background-color: var(--white); // Hide panel contents while resizing .ide-react-editor-content, @@ -188,3 +188,8 @@ opacity: 0.5; } } + +.full-size { + position: absolute; + inset: 0; +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss index 76b1d3c20b..4c03c0147d 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss @@ -5,12 +5,18 @@ --toolbar-btn-color: var(--white); --toolbar-btn-hover-bg-color: var(--neutral-80); --toolbar-btn-hover-color: var(--white); + --toolbar-btn-active-color: var(--white); + --toolbar-btn-active-bg-color: var(--green-50); + --formatting-btn-color: var(--white); + --formatting-btn-bg: var(--neutral-80); + --formatting-btn-border: var(--neutral-70); --project-name-color: var(--neutral-40); --toolbar-filetree-bg-color: var(--neutral-80); --project-rename-link-color: var(--neutral-40); --project-rename-link-color-hover: var(--neutral-20); --editor-header-logo-background: url(../../../../../public/img/ol-brand/overleaf-o-white.svg) center / contain no-repeat; + --editor-toolbar-bg: var(--neutral-80); } @include theme('light') { @@ -20,12 +26,17 @@ --toolbar-btn-color: var(--neutral-70); --toolbar-btn-hover-bg-color: var(--neutral-10); --toolbar-btn-hover-color: var(--neutral-70); + --toolbar-btn-active-bg-color: var(--green-50); + --formatting-btn-color: var(--neutral-70); + --formatting-btn-bg: transparent; + --formatting-btn-border: var(--neutral-20); --project-name-color: var(--neutral-70); --toolbar-filetree-bg-color: var(--white); --project-rename-link-color: var(--neutral-70); --project-rename-link-color-hover: var(--neutral-70); --editor-header-logo-background: url(../../../../../public/img/ol-brand/overleaf-o.svg) center / contain no-repeat; + --editor-toolbar-bg: var(--white); } .toolbar { @@ -300,3 +311,156 @@ } } } + +.toolbar-editor { + height: 32px; + background-color: var(--editor-toolbar-bg); + padding: 0 5px; + overflow: hidden; + position: relative; + z-index: 10; // Prevent track changes showing over toolbar +} + +/************************************** + Toggle Switch +***************************************/ + +.toggle-switch { + display: inline-flex; + align-items: center; + height: 26px; + margin-right: var(--spacing-03); + border-radius: var(--border-radius-full); + background-color: var(--neutral-20); + padding: var(--spacing-01); +} + +.toggle-switch-label { + display: inline-block; + float: left; + font-weight: normal; + height: 100%; + text-align: center; + margin: 0; + cursor: pointer; + user-select: none; + color: var(--content-secondary); + border-radius: var(--border-radius-full); + transition: + color 0.12s ease-out, + background-color 0.12s ease-out, + box-shadow 0.12s ease-out; + overflow: hidden; + + span { + display: flex; + align-items: center; + height: 100%; + width: 100%; + padding: 0 var(--spacing-08); + background-size: 200% 100%; + background-position: 0 0; + transition: background-position 0.12s ease-out; + font-size: var(--font-size-02); + font-weight: bold; + } +} + +.toggle-switch-input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.toggle-switch-input:disabled + .toggle-switch-label { + cursor: not-allowed; +} + +.toggle-switch-input:checked + .toggle-switch-label { + span { + background-position: -100% 0; + } + + color: var(--white); + background-color: var(--bg-accent-01); + border-radius: var(--border-radius-full); + box-shadow: 0 2px 4px rgb(30 37 48 / 16%); +} + +.toggle-switch-input:checked:nth-child(2) + .toggle-switch-label { + span { + background-position: 100% 0; + } +} + +.editor-toggle-switch { + display: flex; + align-items: center; + white-space: nowrap; + + .toggle-switch { + margin-left: var(--spacing-03); + } + + .toggle-switch-label span { + background: none; + transition: background 0.12s ease-out; + } + + .toggle-switch-label:first-of-type span { + padding-left: var(--spacing-04); + } + + .toggle-switch-label:last-of-type span { + padding-right: var(--spacing-04); + border-right: none; + } +} + +/************************************** + Formatting buttons +***************************************/ +.formatting-btn { + color: var(--formatting-btn-color); + background-color: var(--formatting-btn-bg); + padding: 0; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: none; + border: none; + border-left: 1px solid var(--formatting-btn-border); + border-radius: 0; + + &:hover { + color: var(--formatting-btn-color); + } + + &.active { + color: var(--white); + background-color: var(--toolbar-btn-active-bg-color); + box-shadow: none; + + &:focus { + color: var(--toolbar-btn-active-color); + + &:not(:focus-visible) { + outline: none; + } + } + } + + &:focus { + color: var(--formatting-btn-color); + } +} + +.formatting-btn-icon { + min-width: 32px; + width: 32px; +} + +.formatting-btn-icon:last-of-type { + border-right: 1px solid var(--formatting-btn-border); +} diff --git a/services/web/frontend/stylesheets/components/button-groups.less b/services/web/frontend/stylesheets/components/button-groups.less index 24a2fd5bcc..927359d61f 100755 --- a/services/web/frontend/stylesheets/components/button-groups.less +++ b/services/web/frontend/stylesheets/components/button-groups.less @@ -25,6 +25,20 @@ } } +.btn-group { + .btn.active { + background-color: @neutral-20; + } + + .btn[disabled] { + cursor: not-allowed; + background-color: @neutral-20; + border-color: @neutral-20; + opacity: 1; + color: @neutral-90; + } +} + // Prevent double borders when buttons are next to each other .btn-group { .btn + .btn, diff --git a/services/web/frontend/stylesheets/components/switcher.less b/services/web/frontend/stylesheets/components/switcher.less deleted file mode 100644 index e926fcfcb7..0000000000 --- a/services/web/frontend/stylesheets/components/switcher.less +++ /dev/null @@ -1,37 +0,0 @@ -.switcher-input { - position: absolute; - opacity: 0; - pointer-events: none; -} - -.switcher-label { - border: 1px solid black; - padding: 4px 16px; - user-select: none; -} - -.switcher-label:first-of-type { - border-top-left-radius: 999px; - border-bottom-left-radius: 999px; -} - -.switcher-label:last-of-type { - border-top-right-radius: 999px; - border-bottom-right-radius: 999px; -} - -.switcher-label:not(:first-of-type) { - border-left-width: 0px; -} - -.switcher-input:checked + .switcher-label { - background-color: @neutral-20; -} - -.switcher-input:disabled + .switcher-label { - cursor: not-allowed; - background-color: @neutral-20; - border-color: @neutral-20; - opacity: 1; - color: @neutral-90; -} diff --git a/services/web/frontend/stylesheets/main-style.less b/services/web/frontend/stylesheets/main-style.less index 4a48bc3597..a0db92f0a2 100644 --- a/services/web/frontend/stylesheets/main-style.less +++ b/services/web/frontend/stylesheets/main-style.less @@ -72,7 +72,6 @@ @import 'components/list-group.less'; @import 'components/select.less'; @import 'components/switch.less'; -@import 'components/switcher.less'; @import 'components/stepper.less'; @import 'components/radio-chip.less'; @import 'components/interstitial.less'; diff --git a/services/web/locales/en.json b/services/web/locales/en.json index c217e708ba..2a5e9e98df 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -2143,7 +2143,9 @@ "took_a_while": "That took a while...", "toolbar_bullet_list": "Bullet List", "toolbar_choose_section_heading_level": "Choose section heading level", + "toolbar_code_visual_editor_switch": "Code and visual editor switch", "toolbar_decrease_indent": "Decrease Indent", + "toolbar_editor": "Editor tools", "toolbar_format_bold": "Format Bold", "toolbar_format_italic": "Format Italic", "toolbar_increase_indent": "Increase Indent", @@ -2154,7 +2156,10 @@ "toolbar_insert_inline_math": "Insert Inline Math", "toolbar_insert_link": "Insert Link", "toolbar_insert_math": "Insert Math", + "toolbar_insert_math_and_symbols": "Insert Math and Symbols", + "toolbar_insert_misc": "Insert Misc (links, citations, cross-references, figures, tables)", "toolbar_insert_table": "Insert Table", + "toolbar_list_indentation": "List and Indentation", "toolbar_numbered_list": "Numbered List", "toolbar_redo": "Redo", "toolbar_selected_projects": "Selected projects", @@ -2163,8 +2168,12 @@ "toolbar_selected_projects_restore": "Restore selected projects", "toolbar_table_insert_size_table": "Insert __size__ table", "toolbar_table_insert_table_lowercase": "Insert table", + "toolbar_text_formatting": "Text formatting", + "toolbar_text_style": "Text style", "toolbar_toggle_symbol_palette": "Toggle Symbol Palette", "toolbar_undo": "Undo", + "toolbar_undo_redo_actions": "Undo/Redo actions", + "toolbar_visibility": "Toolbar visibility", "tooltip_hide_filetree": "Click to hide the file tree", "tooltip_hide_pdf": "Click to hide the PDF", "tooltip_show_filetree": "Click to show the file tree", diff --git a/services/web/test/frontend/components/shared/split-test-badge.spec.tsx b/services/web/test/frontend/components/shared/split-test-badge.spec.tsx index 8fa09fa44c..173547e4c1 100644 --- a/services/web/test/frontend/components/shared/split-test-badge.spec.tsx +++ b/services/web/test/frontend/components/shared/split-test-badge.spec.tsx @@ -28,9 +28,9 @@ describe('split test badge', function () { ) - cy.get('a.badge.alpha-badge[href="/alpha/participate"]').contains( - 'This is an alpha feature' - ) + cy.findByRole('link', { name: /this is an alpha feature/i }) + .should('have.attr', 'href', '/alpha/participate') + .find('.badge.alpha-badge') }) it('does not render the alpha badge when user is not assigned to the variant', function () { @@ -86,9 +86,9 @@ describe('split test badge', function () { ) - cy.get('a.badge.beta-badge[href="/beta/participate"]').contains( - 'This is a beta feature' - ) + cy.findByRole('link', { name: /this is a beta feature/i }) + .should('have.attr', 'href', '/beta/participate') + .find('.badge.beta-badge') }) it('does not render the beta badge when user is not assigned to the variant', function () { @@ -144,9 +144,9 @@ describe('split test badge', function () { ) - cy.get('a.badge.info-badge[href="/feedback/form"]').contains( - 'This is a new feature' - ) + cy.findByRole('link', { name: /this is a new feature/i }) + .should('have.attr', 'href', '/feedback/form') + .find('.badge.info-badge') }) it('does not render the info badge when user is not assigned to the variant', function () { @@ -218,8 +218,10 @@ describe('split test badge', function () { ) - cy.get('a.badge.info-badge[href="/beta/participate"]') - .contains('We are testing this new feature.') - .contains('Click to give feedback') + cy.findByRole('link', { + name: /we are testing this new feature.*click to give feedback/i, + }) + .should('have.attr', 'href', '/beta/participate') + .find('.badge.info-badge') }) })