Merge pull request #20436 from overleaf/ii-bs5-editor-toolbar

[web] BS5 editor toolbar

GitOrigin-RevId: a517fd52d648d165e89231d6f5551c026a951c43
This commit is contained in:
ilkin-overleaf 2024-09-30 11:11:26 +03:00 committed by Copybot
parent 9aef0cee70
commit 35728d7681
59 changed files with 1764 additions and 764 deletions

View file

@ -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": "",

View file

@ -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 (
<Button
bsStyle={null}
bsSize="xs"
<OLButton
variant="secondary"
size="small"
onClick={handleClick}
className="switch-to-editor-btn toolbar-btn-secondary btn-secondary"
bs3Props={{
bsSize: 'xsmall',
className: 'switch-to-editor-btn toolbar-btn-secondary',
}}
>
<Icon type="code" className="toolbar-btn-secondary-icon" />
<span className="toolbar-btn-secondary-text">
{t('switch_to_editor')}
</span>
</Button>
<BootstrapVersionSwitcher
bs3={<Icon type="code" className="toolbar-btn-secondary-icon" />}
bs5={<MaterialIcon type="code" />}
/>
{t('switch_to_editor')}
</OLButton>
)
}

View file

@ -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 (
<div className="ol-cm-toolbar toolbar-editor" ref={elementRef}>
<div
role="toolbar"
aria-label={t('toolbar_editor')}
className="ol-cm-toolbar toolbar-editor"
ref={elementRef}
>
<EditorSwitch />
{showActions && (
<ToolbarItems
@ -179,19 +187,22 @@ const Toolbar = memo(function Toolbar() {
label="Toggle Search"
command={commands.toggleSearch}
active={searchPanelOpen(state)}
icon="search"
icon={bsVersion({ bs5: 'search', bs3: 'search' }) as string}
/>
<SwitchToPDFButton />
<DetacherSynctexControl />
<DetachCompileButtonWrapper />
</div>
<div className="ol-cm-toolbar-button-group hidden">
<div
className="ol-cm-toolbar-button-group hidden"
aria-label={t('toolbar_visibility')}
>
<ToolbarButton
id="toolbar-expand-less"
label="Hide Toolbar"
command={toggleToolbar}
icon="caret-up"
icon={bsVersion({ bs5: 'arrow_drop_up', bs3: 'caret-up' }) as string}
hidden // enable this once there's a way to show the toolbar again
/>
</div>

View file

@ -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 (
<div className="editor-toggle-switch">
<div
className="editor-toggle-switch"
aria-label={t('toolbar_code_visual_editor_switch')}
>
<fieldset className="toggle-switch">
<legend className="sr-only">Editor mode.</legend>
@ -101,14 +104,14 @@ const RichTextToggle: FC<{
if (disabled) {
return (
<Tooltip
<OLTooltip
description={t('visual_editor_is_only_available_for_tex_files')}
id="rich-text-toggle-tooltip"
overlayProps={{ placement: 'bottom' }}
tooltipProps={{ className: 'tooltip-wide' }}
>
{toggle}
</Tooltip>
</OLTooltip>
)
}

View file

@ -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 && (
<Alert bsStyle="danger" onDismiss={onDismiss}>
{error}
</Alert>
<OLNotification type="error" onDismiss={onDismiss} content={error} />
)}
<Body />
<FigureModalFigureOptions />

View file

@ -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 (
<div className="figure-modal-footer">
<div className="figure-modal-help-buttons">
<HelpToggle />
</div>
<div className="figure-modal-actions">
<Button
bsStyle={null}
className="btn-secondary"
type="button"
onClick={onCancel}
>
{t('cancel')}
</Button>
<FigureModalAction onInsert={onInsert} onDelete={onDelete} />
</div>
</div>
<>
<HelpToggle />
<OLButton variant="secondary" onClick={onCancel}>
{t('cancel')}
</OLButton>
<FigureModalAction onInsert={onInsert} onDelete={onDelete} />
<BootstrapVersionSwitcher bs3={<div className="clearfix" />} />
</>
)
}
@ -39,25 +36,48 @@ const HelpToggle = () => {
const { helpShown, dispatch } = useFigureModalContext()
if (helpShown) {
return (
<Button
bsStyle={null}
className="btn-link figure-modal-help-link"
<OLButton
variant="link"
className={classnames(
'figure-modal-help-link',
bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })
)}
onClick={() => dispatch({ helpShown: false })}
>
<Icon type="arrow-left" fw />
&nbsp;{t('back')}
</Button>
<BootstrapVersionSwitcher
bs3={<Icon type="arrow-left" fw />}
bs5={
<span>
<MaterialIcon
type="arrow_left_alt"
className="align-text-bottom"
/>
</span>
}
/>{' '}
{t('back')}
</OLButton>
)
}
return (
<Button
bsStyle={null}
className="btn-link figure-modal-help-link"
<OLButton
variant="link"
className={classnames(
'figure-modal-help-link',
bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })
)}
onClick={() => dispatch({ helpShown: true })}
>
<Icon type="question-circle" fw />
&nbsp;{t('help')}
</Button>
<BootstrapVersionSwitcher
bs3={<Icon type="question-circle" fw />}
bs5={
<span>
<MaterialIcon type="help" className="align-text-bottom" />
</span>
}
/>{' '}
{t('help')}
</OLButton>
)
}
@ -75,38 +95,29 @@ const FigureModalAction: FC<{
if (sourcePickerShown) {
return (
<Button
bsStyle={null}
className="btn-danger"
type="button"
onClick={onDelete}
>
<OLButton variant="danger" onClick={onDelete}>
{t('delete_figure')}
</Button>
</OLButton>
)
}
if (source === FigureModalSource.EDIT_FIGURE) {
return (
<Button
bsStyle={null}
className="btn-success"
type="button"
<OLButton
variant="primary"
onClick={() => {
onInsert()
sendMB('figure-modal-edit')
}}
>
{t('done')}
</Button>
</OLButton>
)
}
return (
<Button
bsStyle={null}
className="btn-success"
type="button"
<OLButton
variant="primary"
disabled={getPath === undefined}
onClick={() => {
onInsert()
@ -114,6 +125,6 @@ const FigureModalAction: FC<{
}}
>
{t('insert_figure')}
</Button>
</OLButton>
)
}

View file

@ -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 (
<>
<div className="figure-modal-checkbox-input">
<input
type="checkbox"
<OLFormGroup>
<OLFormCheckbox
id="figure-modal-caption"
data-cy="include-caption-option"
defaultChecked={includeCaption}
onChange={event => dispatch({ includeCaption: event.target.checked })}
className={bsVersion({ bs3: 'figure-modal-checkbox-input' })}
label={t('include_caption')}
/>
<label className="figure-modal-label" htmlFor="figure-modal-caption">
{t('include_caption')}
</label>
</div>
<div className="figure-modal-checkbox-input">
<input
type="checkbox"
</OLFormGroup>
<OLFormGroup>
<OLFormCheckbox
id="figure-modal-label"
data-cy="include-label-option"
defaultChecked={includeLabel}
onChange={event => dispatch({ includeLabel: event.target.checked })}
className={bsVersion({ bs3: 'figure-modal-checkbox-input' })}
label={
<span className="figure-modal-label-content">
{t('include_label')}
<br />
<small className="text-muted">
{t(
'used_when_referring_to_the_figure_elsewhere_in_the_document'
)}
</small>
</span>
}
/>
<label htmlFor="figure-modal-label" className="mb-0 figure-modal-label">
{t('include_label')}
<br />
<span className="text-muted text-small figure-modal-label-description">
{t('used_when_referring_to_the_figure_elsewhere_in_the_document')}
</span>
</label>
</div>
<div className="figure-modal-switcher-input">
<div>
{t('image_width')}{' '}
{hasComplexGraphicsArgument ? (
<Tooltip
id="figure-modal-image-width-warning-tooltip"
description={t('a_custom_size_has_been_used_in_the_latex_code')}
overlayProps={{ delay: 0, placement: 'top' }}
>
<Icon type="exclamation-triangle" fw />
</Tooltip>
) : (
<Tooltip
id="figure-modal-image-width-tooltip"
description={t(
'the_width_you_choose_here_is_based_on_the_width_of_the_text_in_your_document'
)}
overlayProps={{ delay: 0, placement: 'bottom' }}
>
<Icon type="question-circle" fw />
</Tooltip>
)}
</div>
<div>
<Switcher
</OLFormGroup>
<OLFormGroup className="mb-0">
<div className="figure-modal-switcher-input">
<div>
{t('image_width')}{' '}
{hasComplexGraphicsArgument ? (
<OLTooltip
id="figure-modal-image-width-warning-tooltip"
description={t('a_custom_size_has_been_used_in_the_latex_code')}
overlayProps={{ delay: 0, placement: 'top' }}
>
<span>
<BootstrapVersionSwitcher
bs3={<Icon type="exclamation-triangle" fw />}
bs5={
<MaterialIcon
type="warning"
className="align-text-bottom"
/>
}
/>
</span>
</OLTooltip>
) : (
<OLTooltip
id="figure-modal-image-width-tooltip"
description={t(
'the_width_you_choose_here_is_based_on_the_width_of_the_text_in_your_document'
)}
overlayProps={{ delay: 0, placement: 'bottom' }}
>
<span>
<BootstrapVersionSwitcher
bs3={<Icon type="question-circle" fw />}
bs5={
<MaterialIcon type="help" className="align-text-bottom" />
}
/>
</span>
</OLTooltip>
)}
</div>
<OLToggleButtonGroup
type="radio"
name="figure-width"
onChange={value => dispatch({ width: parseFloat(value) })}
defaultValue={width === 1 ? '1.0' : width?.toString()}
disabled={hasComplexGraphicsArgument}
aria-label={t('image_width')}
>
<SwitcherItem value="0.25" label={t('1_4_width')} />
<SwitcherItem value="0.5" label={t('1_2_width')} />
<SwitcherItem value="0.75" label={t('3_4_width')} />
<SwitcherItem value="1.0" label={t('full_width')} />
</Switcher>
<OLToggleButton
variant="secondary"
id="width-25p"
disabled={hasComplexGraphicsArgument}
value="0.25"
>
{t('1_4_width')}
</OLToggleButton>
<OLToggleButton
variant="secondary"
id="width-50p"
disabled={hasComplexGraphicsArgument}
value="0.5"
>
{t('1_2_width')}
</OLToggleButton>
<OLToggleButton
variant="secondary"
id="width-75p"
disabled={hasComplexGraphicsArgument}
value="0.75"
>
{t('3_4_width')}
</OLToggleButton>
<OLToggleButton
variant="secondary"
id="width-100p"
disabled={hasComplexGraphicsArgument}
value="1.0"
>
{t('full_width')}
</OLToggleButton>
</OLToggleButtonGroup>
</div>
</div>
</OLFormGroup>
</>
)
}

View file

@ -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 (
<div className="figure-modal-source-selector">
<div className="figure-modal-source-button-grid">
<div className="figure-modal-source-button-grid">
<FigureModalSourceButton
type={FigureModalSource.FILE_UPLOAD}
title={t('replace_from_computer')}
icon={bsVersion({ bs3: 'upload', bs5: 'upload' }) as string}
/>
<FigureModalSourceButton
type={FigureModalSource.FILE_TREE}
title={t('replace_from_project_files')}
icon={bsVersion({ bs3: 'archive', bs5: 'inbox' }) as string}
/>
{(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && (
<FigureModalSourceButton
type={FigureModalSource.FILE_UPLOAD}
title={t('replace_from_computer')}
icon="upload"
type={FigureModalSource.OTHER_PROJECT}
title={t('replace_from_another_project')}
icon={bsVersion({ bs3: 'folder-open', bs5: 'folder_open' }) as string}
/>
)}
{hasLinkUrlFeature && (
<FigureModalSourceButton
type={FigureModalSource.FILE_TREE}
title={t('replace_from_project_files')}
icon="archive"
type={FigureModalSource.FROM_URL}
title={t('replace_from_url')}
icon={bsVersion({ bs3: 'globe', bs5: 'public' }) as string}
/>
{(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && (
<FigureModalSourceButton
type={FigureModalSource.OTHER_PROJECT}
title={t('replace_from_another_project')}
icon="folder-open"
/>
)}
{hasLinkUrlFeature && (
<FigureModalSourceButton
type={FigureModalSource.FROM_URL}
title={t('replace_from_url')}
icon="globe"
/>
)}
</div>
)}
</div>
)
}
@ -54,25 +54,29 @@ const FigureModalSourceButton: FC<{
}> = ({ type, title, icon }) => {
const { dispatch } = useFigureModalContext()
return (
<Button
bsStyle={null}
bsClass=""
<button
type="button"
className="figure-modal-source-button"
onClick={() => {
dispatch({ source: type, sourcePickerShown: false, getPath: undefined })
}}
>
<Icon
type={icon}
className="figure-modal-source-button-icon source-icon"
fw
<BootstrapVersionSwitcher
bs3={
<Icon type={icon} className="figure-modal-source-button-icon" fw />
}
bs5={
<MaterialIcon
type={icon}
className="figure-modal-source-button-icon"
/>
}
/>
<span className="figure-modal-source-button-title">{title}</span>
<Icon
type="chevron-right"
className="figure-modal-source-button-icon"
fw
<BootstrapVersionSwitcher
bs3={<Icon type="chevron-right" fw />}
bs5={<MaterialIcon type="chevron_right" />}
/>
</Button>
</button>
)
}

View file

@ -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 (
<AccessibleModal onHide={hide} className="figure-modal" show>
<Modal.Header closeButton>
<Modal.Title>
<OLModal onHide={hide} className="figure-modal" show>
<OLModalHeader closeButton>
<OLModalTitle>
{helpShown
? t('help')
: sourcePickerShown
@ -281,22 +285,22 @@ const FigureModalContent = () => {
url="https://forms.gle/PfEtwceYBNQ32DF4A"
text="Please click to give feedback about editing figures."
/>
</Modal.Title>
</Modal.Header>
</OLModalTitle>
</OLModalHeader>
<Modal.Body>
<OLModalBody>
<Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}>
<FigureModalBody />
</Suspense>
</Modal.Body>
</OLModalBody>
<Modal.Footer>
<OLModalFooter>
<FigureModalFooter
onInsert={insert}
onCancel={onCancel}
onDelete={onDelete}
/>
</Modal.Footer>
</AccessibleModal>
</OLModalFooter>
</OLModal>
)
}

View file

@ -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<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
React.ComponentProps<typeof OLFormControl>,
'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 (
<>
<input {...props} type="text" onFocus={onFocus} />
{overlap && (
<Alert bsStyle="warning" className="mt-1 mb-0">
{t('a_file_with_that_name_already_exists_and_will_be_overriden')}
</Alert>
)}
<OLFormGroup controlId={id}>
<OLFormLabel>{label}</OLFormLabel>
<OLFormControl onFocus={onFocus} {...props} />
{overlap && (
<OLNotification
type="warning"
content={t(
'a_file_with_that_name_already_exists_and_will_be_overriden'
)}
className="mt-1 mb-0"
/>
)}
</OLFormGroup>
</>
)
}

View file

@ -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 (
<>
<label
htmlFor="figure-modal-relocated-file-name"
className="figure-modal-input-label"
>
{t('file_name_in_this_project_figure_modal')}
</label>
<FileNameInput
id="figure-modal-relocated-file-name"
type="text"
className="form-control figure-modal-input-field"
label={t('file_name_in_this_project_figure_modal')}
value={name}
disabled={nameDisabled}
placeholder="example.jpg"
onChange={nameChanged}
targetFolder={folder}
/>
<Select
items={folders || []}
itemToString={item => {
if (item?.path === '' && item?.name === 'rootFolder') {
<OLFormGroup>
<Select
items={folders || []}
itemToString={item => {
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}
/>
</OLFormGroup>
</>
)
}

View file

@ -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 (
<>
<Select
items={projects ?? []}
itemToString={project => (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,
})
}}
/>
<FileSelector
projectId={selectedProject?._id}
onSelectedItemChange={item => {
const suggestion = nameDirty ? name : suggestName(item?.path ?? '')
setName(suggestion)
setFile(item ?? null)
updateDispatch({
newFile: item ?? null,
newName: suggestion,
})
}}
/>
{hasLinkedProjectFileFeature && hasLinkedProjectOutputFileFeature && (
<div>
or{' '}
<Button
className="p-0"
bsStyle="link"
type="button"
onClick={() => setUsingOutputFiles(value => !value)}
>
<span>
<OLFormGroup>
<Select
items={projects ?? []}
itemToString={project => (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,
})
}}
/>
</OLFormGroup>
<OLFormGroup>
<FileSelector
projectId={selectedProject?._id}
onSelectedItemChange={item => {
const suggestion = nameDirty ? name : suggestName(item?.path ?? '')
setName(suggestion)
setFile(item ?? null)
updateDispatch({
newFile: item ?? null,
newName: suggestion,
})
}}
/>
{hasLinkedProjectFileFeature && hasLinkedProjectOutputFileFeature && (
<div>
or{' '}
<OLButton
variant="link"
onClick={() => setUsingOutputFiles(value => !value)}
className="p-0 select-from-files-btn"
>
{usingOutputFiles
? t('select_from_project_files')
: t('select_from_output_files')}
</span>
</Button>
</div>
)}
</OLButton>
</div>
)}
</OLFormGroup>
<FileRelocator
folder={folder}
name={name}

View file

@ -4,6 +4,7 @@ import { useFigureModalContext } from '../figure-modal-context'
import { filterFiles, isImageFile } from '../../../utils/file'
import { useTranslation } from 'react-i18next'
import { useCurrentProjectFolders } from '@/features/source-editor/hooks/use-current-project-folders'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
export const FigureModalCurrentProjectSource: FC = () => {
const { t } = useTranslation()
@ -15,27 +16,29 @@ export const FigureModalCurrentProjectSource: FC = () => {
const { dispatch, selectedItemId } = useFigureModalContext()
const noFiles = files?.length === 0
return (
<Select
items={files || []}
itemToString={file => (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,
})
}}
/>
<OLFormGroup>
<Select
items={files || []}
itemToString={file => (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,
})
}}
/>
</OLFormGroup>
)
}

View file

@ -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 (
<>
<div className="figure-modal-upload">
{file ? (
<FileContainer
name={file.name}
size={file.size}
status={
uploading
? FileUploadStatus.UPLOADING
: uploadError
? FileUploadStatus.ERROR
: FileUploadStatus.NOT_ATTEMPTED
}
onDelete={() => {
uppy.removeFile(file.id)
setFile(null)
const newName = nameDirty ? name : ''
setName(newName)
dispatchUploadAction(newName, null, folder)
}}
/>
) : (
<Dashboard
uppy={uppy}
showProgressDetails
height={120}
width="100%"
showLinkToFileUploadResult={false}
proudlyDisplayPoweredByUppy={false}
showSelectedFiles={false}
hideUploadButton
locale={{
strings: {
// Text to show on the droppable area.
// `%{browseFiles}` is replaced with a link that opens the system file selection dialog.
dropPasteFiles: `${t(
'drag_here_paste_an_image_or'
)} %{browseFiles}`,
// Used as the label for the link that opens the system file selection dialog.
browseFiles: t('select_from_your_computer'),
},
}}
/>
)}
</div>
<OLFormGroup>
<div className="figure-modal-upload">
{file ? (
<FileContainer
name={file.name}
size={file.size}
status={
uploading
? FileUploadStatus.UPLOADING
: uploadError
? FileUploadStatus.ERROR
: FileUploadStatus.NOT_ATTEMPTED
}
onDelete={() => {
uppy.removeFile(file.id)
setFile(null)
const newName = nameDirty ? name : ''
setName(newName)
dispatchUploadAction(newName, null, folder)
}}
/>
) : (
<Dashboard
uppy={uppy}
showProgressDetails
height={120}
width="100%"
showLinkToFileUploadResult={false}
proudlyDisplayPoweredByUppy={false}
showSelectedFiles={false}
hideUploadButton
locale={{
strings: {
// Text to show on the droppable area.
// `%{browseFiles}` is replaced with a link that opens the system file selection dialog.
dropPasteFiles: `${t(
'drag_here_paste_an_image_or'
)} %{browseFiles}`,
// Used as the label for the link that opens the system file selection dialog.
browseFiles: t('select_from_your_computer'),
},
}}
/>
)}
</div>
</OLFormGroup>
<FileRelocator
folder={folder}
name={name}
@ -266,50 +274,67 @@ export const FileContainer: FC<{
onDelete?: () => 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 (
<div className="file-container">
<div className="file-container-file">
<Icon
spin={status === FileUploadStatus.UPLOADING}
type={icon}
className={classNames(
{
'text-success': status === FileUploadStatus.SUCCESS,
'text-danger': status === FileUploadStatus.ERROR,
},
'file-icon'
<span
className={classNames({
'text-success': status === FileUploadStatus.SUCCESS,
'text-danger': status === FileUploadStatus.ERROR,
})}
>
{status === FileUploadStatus.UPLOADING ? (
<BootstrapVersionSwitcher
bs3={<Icon spin type="spinner" className="file-icon" />}
bs5={
<Spinner
animation="border"
aria-hidden="true"
as="span"
role="status"
size="sm"
/>
}
/>
) : (
<BootstrapVersionSwitcher
bs3={<Icon type={icon} className="file-icon" />}
bs5={<MaterialIcon type={icon} className="align-text-bottom" />}
/>
)}
/>
</span>
<div className="file-info">
<span className="file-name" aria-label={t('file_name_figure_modal')}>
{name}
</span>
{size !== undefined && (
<FileSize size={size} className="text-small" />
)}
{size !== undefined && <FileSize size={size} />}
</div>
<Button
bsStyle={null}
className="btn btn-link p-0"
<OLButton
variant="link"
className="p-0 text-decoration-none"
aria-label={t('remove_or_replace_figure')}
onClick={() => onDelete && onDelete()}
>
<Icon fw type="times-circle" className="file-action file-icon" />
</Button>
<BootstrapVersionSwitcher
bs3={
<Icon fw type="times-circle" className="file-action file-icon" />
}
bs5={<MaterialIcon type="cancel" />}
/>
</OLButton>
</div>
</div>
)
@ -336,8 +361,8 @@ const FileSize: FC<{ size: number; className?: string }> = ({
const [label, bytesPerUnit] = BYTE_UNITS[labelIndex]
const sizeInUnits = Math.round(size / bytesPerUnit)
return (
<span aria-label={t('file_size')} className={className}>
<small aria-label={t('file_size')} className={className}>
{sizeInUnits} {label}
</span>
</small>
)
}

View file

@ -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 (
<>
<label
htmlFor="figure-modal-url-url"
className="figure-modal-input-label"
>
{t('image_url')}
</label>
<input
id="figure-modal-url-url"
type="text"
className="form-control figure-modal-input-field"
placeholder={t('enter_image_url')}
value={url}
onChange={e => {
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)
}}
/>
<OLFormGroup controlId="figure-modal-url-url">
<OLFormLabel>{t('image_url')}</OLFormLabel>
<OLFormControl
type="text"
placeholder={t('enter_image_url')}
value={url}
onChange={e => {
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)
}}
/>
</OLFormGroup>
<FileRelocator
folder={folder}
name={name}

View file

@ -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 SwitchToPDFButton() {
@ -21,15 +23,21 @@ function SwitchToPDFButton() {
}
return (
<Button
bsStyle={null}
bsSize="xs"
<OLButton
variant="secondary"
size="small"
onClick={handleClick}
className="switch-to-pdf-btn toolbar-btn-secondary btn-secondary"
bs3Props={{
bsSize: 'xsmall',
className: 'switch-to-pdf-btn toolbar-btn-secondary',
}}
>
<Icon type="file-pdf-o" className="toolbar-btn-secondary-icon" />
<span className="toolbar-btn-secondary-text">{t('switch_to_pdf')}</span>
</Button>
<BootstrapVersionSwitcher
bs3={<Icon type="file-pdf-o" className="toolbar-btn-secondary-icon" />}
bs5={<MaterialIcon type="picture_as_pdf" />}
/>
{t('switch_to_pdf')}
</OLButton>
)
}

View file

@ -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<any>(null)
const { open, onToggle, ref } = useDropdown()
const view = useCodeMirrorViewContext()
const button = (
<Button
<button
type="button"
className="ol-cm-toolbar-button"
className="ol-cm-toolbar-button btn"
aria-label={label}
bsStyle={null}
onMouseDown={event => {
event.preventDefault()
event.stopPropagation()
@ -48,36 +39,36 @@ export const ToolbarButtonMenu: FC<{
}}
ref={target}
>
{materialIcon ? <MaterialIcon type={icon} /> : <Icon type={icon} fw />}
</Button>
{icon}
</button>
)
const overlay = (
<Overlay
<OLOverlay
show={open}
target={target.current}
placement="bottom"
container={view.dom}
containerPadding={0}
animation
transition
rootClose
onHide={() => onToggle(false)}
>
<Popover
<OLPopover
id={`${id}-menu`}
ref={ref}
className="ol-cm-toolbar-button-menu-popover"
>
<ListGroup
<OLListGroup
role="menu"
onClick={() => {
onToggle(false)
}}
>
{children}
</ListGroup>
</Popover>
</Overlay>
</OLListGroup>
</OLPopover>
</OLOverlay>
)
if (!label) {
@ -91,14 +82,14 @@ export const ToolbarButtonMenu: FC<{
return (
<>
<Tooltip
<OLTooltip
hidden={open}
id={id}
description={<div>{label}</div>}
overlayProps={{ placement: 'bottom' }}
>
{button}
</Tooltip>
</OLTooltip>
{overlay}
</>
)

View file

@ -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() {
<ToolbarButtonMenu
id="toolbar-figure"
label={t('toolbar_insert_figure')}
icon="picture-o"
icon={
<BootstrapVersionSwitcher
bs3={<Icon fw type="picture-o" />}
bs5={<MaterialIcon type="imagesmode" />}
/>
}
altCommand={insertFigure}
>
<ListGroupItem
<OLListGroupItem
onClick={() =>
openFigureModal(FigureModalSource.FILE_UPLOAD, 'file-upload')
}
>
<Icon type="upload" fw />
<BootstrapVersionSwitcher
bs3={<Icon type="upload" fw />}
bs5={<MaterialIcon type="upload" />}
/>
{t('upload_from_computer')}
</ListGroupItem>
<ListGroupItem
</OLListGroupItem>
<OLListGroupItem
onClick={() =>
openFigureModal(FigureModalSource.FILE_TREE, 'current-project')
}
>
<Icon type="archive" fw />
<BootstrapVersionSwitcher
bs3={<Icon type="archive" fw />}
bs5={<MaterialIcon type="inbox" />}
/>
{t('from_project_files')}
</ListGroupItem>
</OLListGroupItem>
{(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && (
<ListGroupItem
<OLListGroupItem
onClick={() =>
openFigureModal(FigureModalSource.OTHER_PROJECT, 'other-project')
}
>
<Icon type="folder-open" fw />
<BootstrapVersionSwitcher
bs3={<Icon type="folder-open" fw />}
bs5={<MaterialIcon type="folder_open" />}
/>
{t('from_another_project')}
</ListGroupItem>
</OLListGroupItem>
)}
{hasLinkUrlFeature && (
<ListGroupItem
<OLListGroupItem
onClick={() =>
openFigureModal(FigureModalSource.FROM_URL, 'from-url')
}
>
<Icon type="globe" fw />
<BootstrapVersionSwitcher
bs3={<Icon type="globe" fw />}
bs5={<MaterialIcon type="public" />}
/>
{t('from_url')}
</ListGroupItem>
</OLListGroupItem>
)}
</ToolbarButtonMenu>
)

View file

@ -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() {
<ToolbarButtonMenu
id="toolbar-math"
label={t('toolbar_insert_math')}
icon="calculate"
materialIcon
icon={<MaterialIcon type="calculate" />}
>
<ListGroupItem
<OLListGroupItem
aria-label={t('toolbar_insert_inline_math')}
onClick={event => {
emitToolbarEvent(view, 'toolbar-inline-math')
@ -32,8 +31,8 @@ export const MathDropdown = memo(function MathDropdown() {
>
<MaterialIcon type="123" />
<span>{t('toolbar_insert_inline_math')}</span>
</ListGroupItem>
<ListGroupItem
</OLListGroupItem>
<OLListGroupItem
aria-label={t('toolbar_insert_display_math')}
onClick={event => {
emitToolbarEvent(view, 'toolbar-display-math')
@ -44,7 +43,7 @@ export const MathDropdown = memo(function MathDropdown() {
>
<MaterialIcon type="view_day" />
<span>{t('toolbar_insert_display_math')}</span>
</ListGroupItem>
</OLListGroupItem>
</ToolbarButtonMenu>
)
})

View file

@ -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<Popover>
overflowRef?: React.Ref<HTMLDivElement>
}> = ({ overflowed, overflowOpen, setOverflowOpen, overflowRef, children }) => {
const buttonRef = useRef<Button>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const view = useCodeMirrorViewContext()
const className = classnames(
'ol-cm-toolbar-button',
'ol-cm-toolbar-overflow-toggle',
bsVersion({ bs3: 'btn' }),
{
'ol-cm-toolbar-overflow-toggle-visible': overflowed,
}
@ -23,13 +28,12 @@ export const ToolbarOverflow: FC<{
return (
<>
<Button
<button
ref={buttonRef}
type="button"
id="toolbar-more"
className={className}
aria-label="More"
bsStyle={null}
onMouseDown={event => {
event.preventDefault()
event.stopPropagation()
@ -38,22 +42,26 @@ export const ToolbarOverflow: FC<{
setOverflowOpen(!overflowOpen)
}}
>
<Icon type="ellipsis-h" fw />
</Button>
<BootstrapVersionSwitcher
bs3={<Icon type="ellipsis-h" fw />}
bs5={<MaterialIcon type="more_horiz" />}
/>
</button>
<Overlay
<OLOverlay
show={overflowOpen}
target={buttonRef.current ?? undefined}
target={buttonRef.current}
placement="bottom"
container={view.dom}
containerPadding={0}
animation
// containerPadding={0}
transition
rootClose
onHide={() => setOverflowOpen(false)}
>
<Popover id="popover-toolbar-overflow" ref={overflowRef}>
<OLPopover id="popover-toolbar-overflow" ref={overflowRef}>
<div className="ol-cm-toolbar-overflow">{children}</div>
</Popover>
</Overlay>
</OLPopover>
</OLOverlay>
</>
)
}

View file

@ -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 = () => {
</button>
{overflowOpen && (
<Overlay
<OLOverlay
show
onHide={() => 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],
},
},
],
}}
>
<Popover
<OLPopover
id="popover-toolbar-section-heading"
className="ol-cm-toolbar-menu-popover"
>
@ -113,8 +124,8 @@ export const SectionHeadingDropdown = () => {
</button>
))}
</div>
</Popover>
</Overlay>
</OLPopover>
</OLOverlay>
)}
</>
)

View file

@ -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 (
<>
<Tooltip
<OLTooltip
hidden={open}
id="toolbar-table"
description={<div>{t('toolbar_insert_table')}</div>}
overlayProps={{ placement: 'bottom' }}
>
<Button
<button
type="button"
className="ol-cm-toolbar-button"
className="ol-cm-toolbar-button btn"
aria-label={t('toolbar_insert_table')}
bsStyle={null}
onMouseDown={event => {
event.preventDefault()
event.stopPropagation()
@ -48,19 +48,19 @@ export const TableInserterDropdown = memo(() => {
ref={target}
>
<MaterialIcon type="table_chart" />
</Button>
</Tooltip>
<Overlay
</button>
</OLTooltip>
<OLOverlay
show={open}
target={target.current}
placement="bottom"
container={view.dom}
containerPadding={0}
animation
transition
rootClose
onHide={() => onToggle(false)}
>
<Popover
<OLPopover
id="toolbar-table-menu"
ref={ref}
className="ol-cm-toolbar-button-menu-popover ol-cm-toolbar-button-menu-popover-unstyled"
@ -68,8 +68,8 @@ export const TableInserterDropdown = memo(() => {
<div className="ol-cm-toolbar-table-grid-popover">
<SizeGrid sizeX={10} sizeY={10} onSizeSelected={onSizeSelected} />
</div>
</Popover>
</Overlay>
</OLPopover>
</OLOverlay>
</>
)
})

View file

@ -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 = (
<Button
className={classnames('ol-cm-toolbar-button', className, { hidden })}
<button
className={classnames(
'ol-cm-toolbar-button',
bsVersion({ bs3: 'btn' }),
className,
{
active,
hidden,
}
)}
aria-label={label}
onMouseDown={handleMouseDown}
onClick={!disabled ? handleClick : undefined}
bsStyle={null}
active={active}
aria-disabled={disabled}
type="button"
>
{textIcon ? icon : <Icon type={icon} fw accessibilityLabel={label} />}
</Button>
{textIcon ? (
icon
) : (
<BootstrapVersionSwitcher
bs3={<Icon type={icon} fw accessibilityLabel={label} />}
bs5={<MaterialIcon type={icon} accessibilityLabel={label} />}
/>
)}
</button>
)
if (!label) {
@ -75,12 +90,12 @@ export const ToolbarButton = memo<{
)
return (
<Tooltip
<OLTooltip
id={id}
description={description}
overlayProps={{ placement: 'bottom' }}
>
{button}
</Tooltip>
</OLTooltip>
)
})

View file

@ -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') && (
<div className="ol-cm-toolbar-button-group">
<div
className="ol-cm-toolbar-button-group"
aria-label={t('toolbar_undo_redo_actions')}
>
<ToolbarButton
id="toolbar-undo"
label={t('toolbar_undo')}
command={undo}
icon="undo"
icon={bsVersion({ bs5: 'undo', bs3: 'undo' }) as string}
shortcut={isMac ? '⌘Z' : 'Ctrl+Z'}
/>
<ToolbarButton
id="toolbar-redo"
label={t('toolbar_redo')}
command={redo}
icon="repeat"
icon={bsVersion({ bs5: 'redo', bs3: 'repeat' }) as string}
shortcut={isMac ? '⇧⌘Z' : 'Ctrl+Y'}
/>
</div>
@ -60,18 +64,22 @@ export const ToolbarItems: FC<{
<div
className="ol-cm-toolbar-button-group"
data-overflow="group-section"
aria-label={t('toolbar_text_formatting')}
>
<SectionHeadingDropdown />
</div>
)}
{showGroup('group-format') && (
<div className="ol-cm-toolbar-button-group">
<div
className="ol-cm-toolbar-button-group"
aria-label={t('toolbar_text_style')}
>
<ToolbarButton
id="toolbar-format-bold"
label={t('toolbar_format_bold')}
command={commands.toggleBold}
active={isActive('\\textbf')}
icon="bold"
icon={bsVersion({ bs5: 'format_bold', bs3: 'bold' }) as string}
shortcut={isMac ? '⌘B' : 'Ctrl+B'}
/>
<ToolbarButton
@ -79,7 +87,9 @@ export const ToolbarItems: FC<{
label={t('toolbar_format_italic')}
command={commands.toggleItalic}
active={isActive('\\textit')}
icon="italic"
icon={
bsVersion({ bs5: 'format_italic', bs3: 'italic' }) as string
}
shortcut={isMac ? '⌘I' : 'Ctrl+I'}
/>
</div>
@ -88,6 +98,7 @@ export const ToolbarItems: FC<{
<div
className="ol-cm-toolbar-button-group"
data-overflow="group-math"
aria-label={t('toolbar_insert_math_and_symbols')}
>
<MathDropdown />
{symbolPaletteAvailable && (
@ -107,24 +118,25 @@ export const ToolbarItems: FC<{
<div
className="ol-cm-toolbar-button-group"
data-overflow="group-misc"
aria-label={t('toolbar_insert_misc')}
>
<ToolbarButton
id="toolbar-href"
label={t('toolbar_insert_link')}
command={commands.wrapInHref}
icon="link"
icon={bsVersion({ bs5: 'link', bs3: 'link' }) as string}
/>
<ToolbarButton
id="toolbar-ref"
label={t('toolbar_insert_cross_reference')}
command={commands.insertRef}
icon="tag"
icon={bsVersion({ bs5: 'sell', bs3: 'tag' }) as string}
/>
<ToolbarButton
id="toolbar-cite"
label={t('toolbar_insert_citation')}
command={commands.insertCite}
icon="book"
icon={bsVersion({ bs5: 'menu_book', bs3: 'book' }) as string}
/>
<InsertFigureDropdown />
<TableInserterDropdown />
@ -134,24 +146,40 @@ export const ToolbarItems: FC<{
<div
className="ol-cm-toolbar-button-group"
data-overflow="group-list"
aria-label={t('toolbar_list_indentation')}
>
<ToolbarButton
id="toolbar-bullet-list"
label={t('toolbar_bullet_list')}
command={commands.toggleBulletList}
icon="list-ul"
icon={
bsVersion({
bs5: 'format_list_bulleted',
bs3: 'list-ul',
}) as string
}
/>
<ToolbarButton
id="toolbar-numbered-list"
label={t('toolbar_numbered_list')}
command={commands.toggleNumberedList}
icon="list-ol"
icon={
bsVersion({
bs5: 'format_list_numbered',
bs3: 'list-ol',
}) as string
}
/>
<ToolbarButton
id="toolbar-format-indent-decrease"
label={t('toolbar_decrease_indent')}
command={commands.indentDecrease}
icon="outdent"
icon={
bsVersion({
bs5: 'format_indent_decrease',
bs3: 'outdent',
}) as string
}
shortcut={visual ? (isMac ? '⌘[' : 'Ctrl+[') : undefined}
disabled={listDepth < 2}
/>
@ -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}
/>

View file

@ -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)',
},

View file

@ -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'

View file

@ -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<T extends string | number>({
children,
value,
defaultValue,
onChange,
...props
}: BS3ToggleButtonGroupProps) {
const [selectedValue, setSelectedValue] = useState<T | T[] | null>(
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 (
<BS3ToggleButtonGroup
{...props}
value={isControlled ? value : undefined}
defaultValue={defaultValue}
// Ignore the broken onChange handler
onChange={() => {}}
>
{modifiedChildren}
</BS3ToggleButtonGroup>
)
}
export default ToggleButtonGroup

View file

@ -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 <BS5Dropdown {...props} />
@ -26,6 +27,7 @@ function DropdownItem(
{
active,
children,
className,
description,
leadingIcon,
trailingIcon,
@ -74,7 +76,9 @@ function DropdownItem(
return (
<BS5DropdownItem
active={active}
className={description ? 'dropdown-item-description-container' : ''}
className={classnames(className, {
'dropdown-item-description-container': description,
})}
role="menuitem"
{...props}
ref={ref}

View file

@ -13,6 +13,7 @@ export type OLButtonProps = ButtonProps & {
loading?: React.ReactNode
bsSize?: BS3ButtonSize
block?: boolean
className?: string
}
}
@ -43,8 +44,8 @@ export const mapBsButtonSizes = (
): 'sm' | 'lg' | undefined =>
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 (
<BootstrapVersionSwitcher
bs3={
<BS3Button {...bs3ButtonProps(rest)} {...bs3Props} {...extraProps}>
<BS3Button
{...bs3ButtonProps({
...rest,
// Override the `className` with bs3 specific className (if provided)
className: bs3Props?.className ?? rest.className,
})}
{...restBs3Props}
{...extraProps}
>
{bs3Props?.loading || rest.children}
</BS3Button>
}

View file

@ -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<unknown>) => void,
inputRef: node => {
if (inputRef) {

View file

@ -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 (
<BootstrapVersionSwitcher
bs3={<BS3ListGroupItem {...bs3ListGroupItemProps} {...extraProps} />}
bs5={
<ListGroupItem
{...rest}
as={as}
type={as === 'button' ? 'button' : undefined}
/>
}
/>
)
}
export default OLListGroupItem

View file

@ -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 (
<BootstrapVersionSwitcher
bs3={<BS3ListGroup {...bs3ListGroupProps} {...extraProps} />}
bs5={<ListGroup {...rest} as={as} />}
/>
)
}
export default OLListGroup

View file

@ -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<OverlayProps['placement'], BS3OverlayProps['placement']>
>
for (const placement of bs3PlacementOptions) {
if (placement === bs5Props.placement) {
bs3OverlayProps.placement = bs5Props.placement
break
}
}
}
bs3OverlayProps = { ...bs3OverlayProps, ...bs3Props }
return (
<BootstrapVersionSwitcher
bs3={<BS3Overlay {...bs3OverlayProps} />}
bs5={<Overlay {...bs5Props} />}
/>
)
}
export default OLOverlay

View file

@ -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<HTMLDivElement, OLPopoverProps>((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<PopoverProps['placement'], BS3PopoverProps['placement']>
>
for (const placement of bs3PlacementOptions) {
if (placement === bs5Props.placement) {
bs3PopoverProps.placement = bs5Props.placement
break
}
}
}
bs3PopoverProps = { ...bs3PopoverProps, ...bs3Props }
return (
<BootstrapVersionSwitcher
bs3={
<BS3Popover
{...bs3PopoverProps}
ref={ref as React.LegacyRef<BS3Popover>}
/>
}
bs5={
<Popover {...bs5Props} ref={ref}>
{title && <Popover.Header>{title}</Popover.Header>}
<Popover.Body>{children}</Popover.Body>
</Popover>
}
/>
)
})
OLPopover.displayName = 'OLPopover'
export default OLPopover

View file

@ -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<T> = ToggleButtonGroupProps<T> & {
bs3Props?: BS3ToggleButtonGroupProps
}
function OLToggleButtonGroup<T>(props: OLToggleButtonGroupProps<T>) {
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 (
<BootstrapVersionSwitcher
bs3={
<BS3ToggleButtonGroup {...extraProps} {...bs3ToggleButtonGroupProps} />
}
bs5={<ToggleButtonGroup {...rest} />}
/>
)
}
export default OLToggleButtonGroup

View file

@ -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 (
<BootstrapVersionSwitcher
bs3={
<BS3ToggleButton
{...extraProps}
{...bs3ToggleButtonProps}
bsStyle={null}
/>
}
bs5={<ToggleButton {...rest} />}
/>
)
}
export default OLToggleButton

View file

@ -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<typeof OLTooltip>['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
<OLTooltip
id={tooltip.id}
description={tooltip.text}
tooltipProps={{ className: tooltip.className }}
overlayProps={{
placement: tooltip.placement || 'bottom',
delayHide: 100,
delay: 100,
}}
>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className={classnames('badge', badgeClass)}
>
<a href={url} target="_blank" rel="noopener noreferrer">
<span className="sr-only">{tooltip.text}</span>
<BootstrapVersionSwitcher
bs3={<span className={classnames('badge', badgeClass)} />}
bs5={
<MaterialIcon
type="info"
className={classnames('align-middle', badgeClass)}
/>
}
/>
</a>
</Tooltip>
</OLTooltip>
)
}

View file

@ -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<T> = {
// The items rendered as dropdown options.
@ -79,6 +84,7 @@ export const Select = <T,>({
getItemProps,
highlightedIndex,
openMenu,
closeMenu,
} = useSelect({
items: items ?? [],
itemToString,
@ -117,9 +123,16 @@ export const Select = <T,>({
}
}, [name, itemToString, selectedItem, defaultItem])
const handleMenuKeyDown = (event: React.KeyboardEvent<HTMLUListElement>) => {
if (event.key === 'Escape' && isOpen) {
event.stopPropagation()
closeMenu()
}
}
const onKeyDown: KeyboardEventHandler<HTMLButtonElement> = 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 = <T,>({
value = defaultText
}
return (
<div className="select-wrapper" ref={rootRef}>
<div>
{label ? (
<label {...getLabelProps()}>
{label}{' '}
{optionalLabel && (
<span className="select-optional-label text-muted">
({t('optional')})
</span>
)}{' '}
{loading && <Icon data-testid="spinner" fw type="spinner" spin />}
</label>
) : null}
<div
className={classNames({ disabled }, 'select-trigger')}
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
{...getToggleButtonProps({
disabled,
onKeyDown,
})}
>
<div>{value}</div>
<BootstrapVersionSwitcher
bs3={
<div className="select-wrapper" ref={rootRef}>
<div>
{isOpen ? (
<Icon type="chevron-up" fw />
) : (
<Icon type="chevron-down" fw />
)}
</div>
</div>
</div>
<ul
className={classNames({ hidden: !isOpen }, 'select-items')}
{...getMenuProps({ disabled })}
>
{isOpen &&
items?.map((item, index) => {
const isDisabled = itemToDisabled && itemToDisabled(item)
return (
<li
className={classNames({
'select-highlighted': highlightedIndex === index,
'selected-active': selectedItem === item,
'select-icon': selectedIcon,
'select-disabled': isDisabled,
})}
key={itemToKey(item)}
{...getItemProps({ item, index, disabled: isDisabled })}
>
<span className="select-item-title">
{selectedIcon && (
<div className="select-item-icon">
{(selectedItem === item ||
(!selectedItem && defaultItem === item)) && (
<Icon type="check" fw />
)}
</div>
)}
{itemToString(item)}
</span>
{itemToSubtitle ? (
<span className="text-muted select-item-subtitle">
{itemToSubtitle(item)}
{label ? (
<label {...getLabelProps()}>
{label}{' '}
{optionalLabel && (
<span className="select-optional-label text-muted">
({t('optional')})
</span>
) : null}
</li>
)
})}
</ul>
</div>
)}{' '}
{loading && (
<Icon data-testid="spinner" fw type="spinner" spin />
)}
</label>
) : null}
<div
className={classNames({ disabled }, 'select-trigger')}
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
{...getToggleButtonProps({
disabled,
onKeyDown,
})}
>
<div>{value}</div>
<div>
{isOpen ? (
<Icon type="chevron-up" fw />
) : (
<Icon type="chevron-down" fw />
)}
</div>
</div>
</div>
<ul
className={classNames({ hidden: !isOpen }, 'select-items')}
{...getMenuProps({ disabled, onKeyDown: handleMenuKeyDown })}
>
{isOpen &&
items?.map((item, index) => {
const isDisabled = itemToDisabled && itemToDisabled(item)
return (
<li
className={classNames({
'select-highlighted': highlightedIndex === index,
'selected-active': selectedItem === item,
'select-icon': selectedIcon,
'select-disabled': isDisabled,
})}
key={itemToKey(item)}
{...getItemProps({ item, index, disabled: isDisabled })}
>
<span className="select-item-title">
{selectedIcon && (
<div className="select-item-icon">
{(selectedItem === item ||
(!selectedItem && defaultItem === item)) && (
<Icon type="check" fw />
)}
</div>
)}
{itemToString(item)}
</span>
{itemToSubtitle ? (
<span className="text-muted select-item-subtitle">
{itemToSubtitle(item)}
</span>
) : null}
</li>
)
})}
</ul>
</div>
}
bs5={
<div className="select-wrapper" ref={rootRef}>
{label ? (
<Form.Label {...getLabelProps()}>
{label}{' '}
{optionalLabel && (
<span className="fw-normal">({t('optional')})</span>
)}{' '}
{loading && (
<span data-testid="spinner">
<Spinner
animation="border"
aria-hidden="true"
as="span"
role="status"
size="sm"
/>
</span>
)}
</Form.Label>
) : null}
<FormControl
{...getToggleButtonProps({
disabled,
onKeyDown,
className: 'select-trigger',
})}
value={value}
readOnly
append={
<MaterialIcon
type={isOpen ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
className="align-text-bottom"
/>
}
/>
<ul
{...getMenuProps({ disabled, onKeyDown: handleMenuKeyDown })}
className={classNames('dropdown-menu w-100', { show: isOpen })}
>
{isOpen &&
items?.map((item, index) => {
const isDisabled = itemToDisabled && itemToDisabled(item)
return (
<li role="none" key={itemToKey(item)}>
<DropdownItem
as="button"
className={classNames({
'select-highlighted': highlightedIndex === index,
})}
active={selectedItem === item}
trailingIcon={selectedItem === item ? 'check' : undefined}
description={
itemToSubtitle ? itemToSubtitle(item) : undefined
}
{...getItemProps({ item, index, disabled: isDisabled })}
>
{itemToString(item)}
</DropdownItem>
</li>
)
})}
</ul>
</div>
}
/>
)
}

View file

@ -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 (
<SwitcherContext.Provider
value={{ name, onChange, defaultValue, disabled }}
>
<fieldset>{children}</fieldset>
</SwitcherContext.Provider>
)
}
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 (
<>
<input
type="radio"
value={value}
id={id}
className="switcher-input"
name={name}
defaultChecked={!disabled && (checked || defaultValue === value)}
disabled={disabled}
onChange={evt => {
if (onChange) {
onChange(evt.target.value)
}
}}
/>
<label htmlFor={id} className="switcher-label" aria-disabled={disabled}>
<span>{label}</span>
</label>
</>
)
}

View file

@ -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 (
<Switcher name="figure-width" defaultValue="0.5">
<SwitcherItem value="0.25" label="¼ width" />
<SwitcherItem value="0.5" label="½ width" />
<SwitcherItem value="0.75" label="¾ width" />
<SwitcherItem value="1.0" label="Full width" />
</Switcher>
<OLToggleButtonGroup
type="radio"
name="figure-width"
defaultValue="0.5"
aria-label="Image width"
>
<OLToggleButton variant="secondary" id="width-25p" value="0.25">
¼ width
</OLToggleButton>
<OLToggleButton variant="secondary" id="width-50p" value="0.5">
½ width
</OLToggleButton>
<OLToggleButton variant="secondary" id="width-75p" value="0.75">
¾ width
</OLToggleButton>
<OLToggleButton variant="secondary" id="width-100p" value="1.0">
Full width
</OLToggleButton>
</OLToggleButtonGroup>
)
}
export const Disabled = () => {
return (
<Switcher name="figure-width" defaultValue="0.5" disabled>
<SwitcherItem value="0.25" label="¼ width" />
<SwitcherItem value="0.5" label="½ width" />
<SwitcherItem value="0.75" label="¾ width" />
<SwitcherItem value="1.0" label="Full width" />
</Switcher>
<OLToggleButtonGroup
type="radio"
name="figure-width"
defaultValue="0.5"
aria-label="Image width"
>
<OLToggleButton variant="secondary" id="width-25p" disabled value="0.25">
¼ width
</OLToggleButton>
<OLToggleButton variant="secondary" id="width-50p" disabled value="0.5">
½ width
</OLToggleButton>
<OLToggleButton variant="secondary" id="width-75p" disabled value="0.75">
¾ width
</OLToggleButton>
<OLToggleButton variant="secondary" id="width-100p" disabled value="1.0">
Full width
</OLToggleButton>
</OLToggleButtonGroup>
)
}
export default {
title: 'Shared / Components / Switcher',
component: Switcher,
title: 'Shared / Components / Toggle Button Group',
component: OLToggleButtonGroup,
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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';

View file

@ -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';

View file

@ -0,0 +1,11 @@
.info-badge {
color: var(--blue-50);
}
.alpha-badge {
color: var(--green-50);
}
.beta-badge {
color: var(--yellow-40);
}

View file

@ -84,7 +84,7 @@
}
.form-label {
&:has(+ .form-control[disabled]) {
&:has(+ .form-control[disabled], + .form-control-wrapper-disabled) {
color: var(--content-disabled);
}
}

View file

@ -0,0 +1,3 @@
.list-group-item {
min-height: 48px;
}

View file

@ -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;
}
}

View file

@ -0,0 +1,11 @@
.select-wrapper {
position: relative;
}
.select-trigger {
cursor: default;
}
.select-highlighted {
background-color: var(--bg-light-secondary);
}

View file

@ -10,4 +10,5 @@
@import 'editor/loading-screen';
@import 'editor/outline';
@import 'editor/file-tree';
@import 'editor/figure-modal';
@import 'website-redesign';

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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';

View file

@ -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",

View file

@ -28,9 +28,9 @@ describe('split test badge', function () {
</EditorProviders>
)
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 () {
</EditorProviders>
)
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 () {
</EditorProviders>
)
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 () {
</EditorProviders>
)
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')
})
})