Merge pull request #20708 from overleaf/ii-bs5-pdf-toolbar

[web] BS5 pdf toolbar

GitOrigin-RevId: a04091c9e936e52f47c3977b3149ffe613d43bb9
This commit is contained in:
ilkin-overleaf 2024-10-08 17:22:13 +03:00 committed by Copybot
parent 9ce7d4ec44
commit 92eade7502
54 changed files with 1384 additions and 595 deletions

View file

@ -1,4 +1,4 @@
extends ../layout-marketing extends ../layout-react
block entrypointVar block entrypointVar
- entrypoint = 'ide-detached' - entrypoint = 'ide-detached'

View file

@ -1,4 +1,4 @@
extends ../layout-marketing extends ../layout-react
block vars block vars
- var suppressNavbar = true - var suppressNavbar = true

View file

@ -8,7 +8,7 @@ function BackToEditorButton({ onClick }: { onClick: () => void }) {
return ( return (
<OLButton <OLButton
variant="secondary" variant="secondary"
size="small" size="sm"
onClick={onClick} onClick={onClick}
className="back-to-editor-btn" className="back-to-editor-btn"
> >

View file

@ -15,7 +15,7 @@ function UpgradePrompt() {
return ( return (
<OLButton <OLButton
variant="primary" variant="primary"
size="small" size="sm"
className={classnames( className={classnames(
'toolbar-header-upgrade-prompt', 'toolbar-header-upgrade-prompt',
bsVersion({ bs3: 'btn-xs' }) bsVersion({ bs3: 'btn-xs' })

View file

@ -1,10 +1,12 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { memo } from 'react' import { memo } from 'react'
import { Button } from 'react-bootstrap' import classnames from 'classnames'
import classNames from 'classnames'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { useDetachCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext } from '../../../shared/context/detach-compile-context'
import Tooltip from '../../../shared/components/tooltip' import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLButton from '@/features/ui/components/ol/ol-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl' const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
@ -21,28 +23,49 @@ function DetachCompileButton() {
) )
return ( return (
<div className="detach-compile-button-container"> <div
<Tooltip className={classnames(
'detach-compile-button-container',
bsVersion({ bs5: 'ms-1' })
)}
>
<OLTooltip
id="detach-compile" id="detach-compile"
description={tooltipElement} description={tooltipElement}
tooltipProps={{ className: 'keyboard-tooltip' }} tooltipProps={{ className: 'keyboard-tooltip' }}
overlayProps={{ delayShow: 500 }} overlayProps={{ delay: { show: 500, hide: 0 } }}
> >
<Button <OLButton
bsStyle="primary" variant="primary"
onClick={() => startCompile()} onClick={() => startCompile()}
disabled={compiling} disabled={compiling}
className={classNames('detach-compile-button', { className={classnames('detach-compile-button', {
'btn-striped-animated': hasChanges, 'btn-striped-animated': hasChanges,
'detach-compile-button-disabled': compiling, 'detach-compile-button-disabled': compiling,
})} })}
size="sm"
isLoading={compiling}
bs3Props={{
loading: (
<>
<Icon type="refresh" spin={compiling} />
<span className="detach-compile-button-label">
{compileButtonLabel}
</span>
</>
),
}}
> >
<Icon type="refresh" spin={compiling} /> <BootstrapVersionSwitcher
<span className="detach-compile-button-label"> bs3={
{compileButtonLabel} <span className="detach-compile-button-label">
</span> {compileButtonLabel}
</Button> </span>
</Tooltip> }
bs5={compileButtonLabel}
/>
</OLButton>
</OLTooltip>
</div> </div>
) )
} }

View file

@ -1,179 +0,0 @@
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import classNames from 'classnames'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
import SplitMenu from '../../../shared/components/split-menu'
import Icon from '../../../shared/components/icon'
import * as eventTracking from '../../../infrastructure/event-tracking'
const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
function sendEventAndSet(value, setter, settingName) {
eventTracking.sendMB('recompile-setting-changed', {
setting: settingName,
settingVal: value,
})
setter(value)
}
function PdfCompileButton() {
const {
animateCompileDropdownArrow,
autoCompile,
compiling,
draft,
hasChanges,
setAnimateCompileDropdownArrow,
setAutoCompile,
setDraft,
setStopOnValidationError,
stopOnFirstError,
stopOnValidationError,
startCompile,
stopCompile,
recompileFromScratch,
} = useCompileContext()
const { enableStopOnFirstError, disableStopOnFirstError } =
useStopOnFirstError({ eventSource: 'dropdown' })
const { t } = useTranslation()
const fromScratchWithEvent = () => {
eventTracking.sendMB('recompile-setting-changed', {
setting: 'from-scratch',
})
recompileFromScratch()
}
const compileButtonLabel = compiling ? `${t('compiling')}` : t('recompile')
const tooltipElement = (
<>
{t('recompile_pdf')}{' '}
<span className="keyboard-shortcut">({modifierKey} + Enter)</span>
</>
)
const dropdownToggleClassName = classNames({
'detach-compile-button-animate': animateCompileDropdownArrow,
'btn-striped-animated': hasChanges,
'no-left-border': true,
})
const buttonClassName = classNames({
'btn-striped-animated': hasChanges,
'no-left-radius': true,
})
return (
<SplitMenu
bsStyle="primary"
bsSize="xs"
disabled={compiling}
button={{
tooltip: {
description: tooltipElement,
id: 'compile',
tooltipProps: { className: 'keyboard-tooltip' },
overlayProps: { delayShow: 500 },
},
icon: { type: 'refresh', spin: compiling },
onClick: () => startCompile(),
text: compileButtonLabel,
className: buttonClassName,
}}
dropdownToggle={{
'aria-label': t('toggle_compile_options_menu'),
handleAnimationEnd: () => setAnimateCompileDropdownArrow(false),
className: dropdownToggleClassName,
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item header>{t('auto_compile')}</SplitMenu.Item>
<SplitMenu.Item
onSelect={() => sendEventAndSet(true, setAutoCompile, 'auto-compile')}
>
<Icon type={autoCompile ? 'check' : ''} fw />
{t('on')}
</SplitMenu.Item>
<SplitMenu.Item
onSelect={() => sendEventAndSet(false, setAutoCompile, 'auto-compile')}
>
<Icon type={!autoCompile ? 'check' : ''} fw />
{t('off')}
</SplitMenu.Item>
<SplitMenu.Item header>{t('compile_mode')}</SplitMenu.Item>
<SplitMenu.Item
onSelect={() => sendEventAndSet(false, setDraft, 'compile-mode')}
>
<Icon type={!draft ? 'check' : ''} fw />
{t('normal')}
</SplitMenu.Item>
<SplitMenu.Item
onSelect={() => sendEventAndSet(true, setDraft, 'compile-mode')}
>
<Icon type={draft ? 'check' : ''} fw />
{t('fast')} <span className="subdued">[draft]</span>
</SplitMenu.Item>
<SplitMenu.Item header>Syntax Checks</SplitMenu.Item>
<SplitMenu.Item
onSelect={() =>
sendEventAndSet(true, setStopOnValidationError, 'syntax-check')
}
>
<Icon type={stopOnValidationError ? 'check' : ''} fw />
{t('stop_on_validation_error')}
</SplitMenu.Item>
<SplitMenu.Item
onSelect={() =>
sendEventAndSet(false, setStopOnValidationError, 'syntax-check')
}
>
<Icon type={!stopOnValidationError ? 'check' : ''} fw />
{t('ignore_validation_errors')}
</SplitMenu.Item>
<SplitMenu.Item header>{t('compile_error_handling')}</SplitMenu.Item>
<SplitMenu.Item onSelect={enableStopOnFirstError}>
<Icon type={stopOnFirstError ? 'check' : ''} fw />
{t('stop_on_first_error')}
</SplitMenu.Item>
<SplitMenu.Item onSelect={disableStopOnFirstError}>
<Icon type={!stopOnFirstError ? 'check' : ''} fw />
{t('try_to_compile_despite_errors')}
</SplitMenu.Item>
<SplitMenu.Item divider />
<SplitMenu.Item
onSelect={() => stopCompile()}
disabled={!compiling}
aria-disabled={!compiling}
>
{t('stop_compile')}
</SplitMenu.Item>
<SplitMenu.Item
onSelect={fromScratchWithEvent}
disabled={compiling}
aria-disabled={compiling}
>
{t('recompile_from_scratch')}
</SplitMenu.Item>
</SplitMenu>
)
}
export default memo(PdfCompileButton)

View file

@ -0,0 +1,366 @@
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import classNames from 'classnames'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
import SplitMenu from '../../../shared/components/split-menu'
import Icon from '../../../shared/components/icon'
import * as eventTracking from '../../../infrastructure/event-tracking'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import { Spinner } from 'react-bootstrap-5'
import {
Dropdown,
DropdownDivider,
DropdownHeader,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLButtonGroup from '@/features/ui/components/ol/ol-button-group'
import { bsVersion } from '@/features/utils/bootstrap-5'
const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
function sendEventAndSet<T extends boolean>(
value: T,
setter: (value: T) => void,
settingName: string
) {
eventTracking.sendMB('recompile-setting-changed', {
setting: settingName,
settingVal: value,
})
setter(value)
}
function PdfCompileButton() {
const {
animateCompileDropdownArrow,
autoCompile,
compiling,
draft,
hasChanges,
setAnimateCompileDropdownArrow,
setAutoCompile,
setDraft,
setStopOnValidationError,
stopOnFirstError,
stopOnValidationError,
startCompile,
stopCompile,
recompileFromScratch,
} = useCompileContext()
const { enableStopOnFirstError, disableStopOnFirstError } =
useStopOnFirstError({ eventSource: 'dropdown' })
const { t } = useTranslation()
const fromScratchWithEvent = () => {
eventTracking.sendMB('recompile-setting-changed', {
setting: 'from-scratch',
})
recompileFromScratch()
}
const compileButtonLabel = compiling ? `${t('compiling')}` : t('recompile')
const tooltipElement = (
<>
{t('recompile_pdf')}{' '}
<span className="keyboard-shortcut">({modifierKey} + Enter)</span>
</>
)
const dropdownToggleClassName = classNames(
{
'detach-compile-button-animate': animateCompileDropdownArrow,
'btn-striped-animated': hasChanges,
'no-left-border': true,
},
bsVersion({ bs5: 'dropdown-button-toggle' })
)
const buttonClassName = classNames({
'btn-striped-animated': hasChanges,
'no-left-radius': true,
})
return (
<BootstrapVersionSwitcher
bs3={
<SplitMenu
bsStyle="primary"
bsSize="xs"
disabled={compiling}
button={{
tooltip: {
description: tooltipElement,
id: 'compile',
tooltipProps: { className: 'keyboard-tooltip' },
overlayProps: { delayShow: 500 },
},
icon: { type: 'refresh', spin: compiling },
onClick: () => startCompile(),
text: compileButtonLabel,
className: buttonClassName,
}}
dropdownToggle={{
'aria-label': t('toggle_compile_options_menu'),
handleAnimationEnd: () => setAnimateCompileDropdownArrow(false),
className: dropdownToggleClassName,
}}
dropdown={{
id: 'pdf-recompile-dropdown',
}}
>
<SplitMenu.Item header>{t('auto_compile')}</SplitMenu.Item>
<SplitMenu.Item
onSelect={() =>
sendEventAndSet(true, setAutoCompile, 'auto-compile')
}
>
<Icon type={autoCompile ? 'check' : ''} fw />
{t('on')}
</SplitMenu.Item>
<SplitMenu.Item
onSelect={() =>
sendEventAndSet(false, setAutoCompile, 'auto-compile')
}
>
<Icon type={!autoCompile ? 'check' : ''} fw />
{t('off')}
</SplitMenu.Item>
<SplitMenu.Item header>{t('compile_mode')}</SplitMenu.Item>
<SplitMenu.Item
onSelect={() => sendEventAndSet(false, setDraft, 'compile-mode')}
>
<Icon type={!draft ? 'check' : ''} fw />
{t('normal')}
</SplitMenu.Item>
<SplitMenu.Item
onSelect={() => sendEventAndSet(true, setDraft, 'compile-mode')}
>
<Icon type={draft ? 'check' : ''} fw />
{t('fast')} <span className="subdued">[draft]</span>
</SplitMenu.Item>
<SplitMenu.Item header>Syntax Checks</SplitMenu.Item>
<SplitMenu.Item
onSelect={() =>
sendEventAndSet(true, setStopOnValidationError, 'syntax-check')
}
>
<Icon type={stopOnValidationError ? 'check' : ''} fw />
{t('stop_on_validation_error')}
</SplitMenu.Item>
<SplitMenu.Item
onSelect={() =>
sendEventAndSet(false, setStopOnValidationError, 'syntax-check')
}
>
<Icon type={!stopOnValidationError ? 'check' : ''} fw />
{t('ignore_validation_errors')}
</SplitMenu.Item>
<SplitMenu.Item header>{t('compile_error_handling')}</SplitMenu.Item>
<SplitMenu.Item onSelect={enableStopOnFirstError}>
<Icon type={stopOnFirstError ? 'check' : ''} fw />
{t('stop_on_first_error')}
</SplitMenu.Item>
<SplitMenu.Item onSelect={disableStopOnFirstError}>
<Icon type={!stopOnFirstError ? 'check' : ''} fw />
{t('try_to_compile_despite_errors')}
</SplitMenu.Item>
<SplitMenu.Item divider />
<SplitMenu.Item
onSelect={() => stopCompile()}
disabled={!compiling}
aria-disabled={!compiling}
>
{t('stop_compile')}
</SplitMenu.Item>
<SplitMenu.Item
onSelect={fromScratchWithEvent}
disabled={compiling}
aria-disabled={compiling}
>
{t('recompile_from_scratch')}
</SplitMenu.Item>
</SplitMenu>
}
bs5={
<Dropdown as={OLButtonGroup} autoClose="outside">
<OLTooltip
description={tooltipElement}
id="compile"
tooltipProps={{ className: 'keyboard-tooltip' }}
overlayProps={{ delay: { show: 500, hide: 0 } }}
>
<OLButton
variant="primary"
size="sm"
disabled={compiling}
onClick={() => startCompile()}
leadingIcon={
compiling ? (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
) : null
}
className={buttonClassName}
>
{compileButtonLabel}
</OLButton>
</OLTooltip>
<DropdownToggle
split
variant="primary"
id="pdf-recompile-dropdown"
size="sm"
aria-label={t('toggle_compile_options_menu')}
className={dropdownToggleClassName}
/>
<DropdownMenu>
<DropdownHeader>{t('auto_compile')}</DropdownHeader>
<li role="none">
<DropdownItem
as="button"
onClick={() =>
sendEventAndSet(true, setAutoCompile, 'auto-compile')
}
trailingIcon={autoCompile ? 'check' : null}
>
{t('on')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={() =>
sendEventAndSet(false, setAutoCompile, 'auto-compile')
}
trailingIcon={!autoCompile ? 'check' : null}
>
{t('off')}
</DropdownItem>
</li>
<DropdownDivider />
<DropdownHeader>{t('compile_mode')}</DropdownHeader>
<li role="none">
<DropdownItem
as="button"
onClick={() => sendEventAndSet(false, setDraft, 'compile-mode')}
trailingIcon={!draft ? 'check' : null}
>
{t('normal')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={() => sendEventAndSet(true, setDraft, 'compile-mode')}
trailingIcon={draft ? 'check' : null}
>
{t('fast')}&nbsp;<span className="subdued">[draft]</span>
</DropdownItem>
</li>
<DropdownDivider />
<DropdownHeader>Syntax Checks</DropdownHeader>
<li role="none">
<DropdownItem
as="button"
onClick={() =>
sendEventAndSet(
true,
setStopOnValidationError,
'syntax-check'
)
}
trailingIcon={stopOnValidationError ? 'check' : null}
>
{t('stop_on_validation_error')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={() =>
sendEventAndSet(
false,
setStopOnValidationError,
'syntax-check'
)
}
trailingIcon={!stopOnValidationError ? 'check' : null}
>
{t('ignore_validation_errors')}
</DropdownItem>
</li>
<DropdownDivider />
<DropdownHeader>{t('compile_error_handling')}</DropdownHeader>
<li role="none">
<DropdownItem
as="button"
onClick={enableStopOnFirstError}
trailingIcon={stopOnFirstError ? 'check' : null}
>
{t('stop_on_first_error')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={disableStopOnFirstError}
trailingIcon={!stopOnFirstError ? 'check' : null}
>
{t('try_to_compile_despite_errors')}
</DropdownItem>
</li>
<DropdownDivider />
<li role="none">
<DropdownItem
as="button"
onClick={() => stopCompile()}
disabled={!compiling}
aria-disabled={!compiling}
>
{t('stop_compile')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={fromScratchWithEvent}
disabled={compiling}
aria-disabled={compiling}
>
{t('recompile_from_scratch')}
</DropdownItem>
</li>
</DropdownMenu>
</Dropdown>
}
/>
)
}
export default memo(PdfCompileButton)

View file

@ -1,8 +1,11 @@
import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import { Button } from 'react-bootstrap'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import 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'
function PdfHybridCodeCheckButton() { function PdfHybridCodeCheckButton() {
const { codeCheckFailed, error, toggleLogs } = useCompileContext() const { codeCheckFailed, error, toggleLogs } = useCompileContext()
@ -18,18 +21,24 @@ function PdfHybridCodeCheckButton() {
} }
return ( return (
<Button <OLButton
bsSize="xsmall" variant="danger"
bsStyle="danger" size="sm"
disabled={Boolean(error)} disabled={Boolean(error)}
className="btn-toggle-logs toolbar-item" className="btn-toggle-logs toolbar-item"
onClick={handleClick} onClick={handleClick}
bs3Props={{
bsSize: 'xsmall',
}}
> >
<Icon type="exclamation-triangle" /> <BootstrapVersionSwitcher
<span className="toolbar-text toolbar-hide-small"> bs3={<Icon type="exclamation-triangle" />}
bs5={<MaterialIcon type="warning" />}
/>
<span className={bsVersion({ bs3: 'toolbar-text' })}>
{t('code_check_failed')} {t('code_check_failed')}
</span> </span>
</Button> </OLButton>
) )
} }

View file

@ -1,11 +1,12 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap'
import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import * as eventTracking from '../../../infrastructure/event-tracking' import { sendMB, isSmallDevice } from '@/infrastructure/event-tracking'
import { isSmallDevice } from '../../../infrastructure/event-tracking' import Icon from '@/shared/components/icon'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLButton from '@/features/ui/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function PdfHybridDownloadButton() { function PdfHybridDownloadButton() {
const { pdfDownloadUrl } = useCompileContext() const { pdfDownloadUrl } = useCompileContext()
@ -17,8 +18,14 @@ function PdfHybridDownloadButton() {
? t('download_pdf') ? t('download_pdf')
: t('please_compile_pdf_before_download') : t('please_compile_pdf_before_download')
function handleOnClick() { function handleOnClick(e: React.MouseEvent) {
eventTracking.sendMB('download-pdf-button-click', { const event = e as React.MouseEvent<HTMLAnchorElement>
if (event.currentTarget.dataset.disabled === 'true') {
event.preventDefault()
return
}
sendMB('download-pdf-button-click', {
projectId, projectId,
location: 'pdf-preview', location: 'pdf-preview',
isSmallDevice, isSmallDevice,
@ -26,16 +33,17 @@ function PdfHybridDownloadButton() {
} }
return ( return (
<Tooltip <OLTooltip
id="download-pdf" id="download-pdf"
description={description} description={description}
overlayProps={{ placement: 'bottom' }} overlayProps={{ placement: 'bottom' }}
> >
<Button <OLButton
onClick={handleOnClick} onClick={handleOnClick}
bsStyle="link" variant="link"
className="pdf-toolbar-btn" className="pdf-toolbar-btn"
draggable="false" draggable={false}
data-disabled={!pdfDownloadUrl}
disabled={!pdfDownloadUrl} disabled={!pdfDownloadUrl}
download download
href={pdfDownloadUrl || '#'} href={pdfDownloadUrl || '#'}
@ -43,9 +51,12 @@ function PdfHybridDownloadButton() {
style={{ pointerEvents: 'auto' }} style={{ pointerEvents: 'auto' }}
aria-label={t('download_pdf')} aria-label={t('download_pdf')}
> >
<Icon type="download" fw /> <BootstrapVersionSwitcher
</Button> bs3={<Icon type="download" fw />}
</Tooltip> bs5={<MaterialIcon type="download" />}
/>
</OLButton>
</OLTooltip>
) )
} }

View file

@ -1,10 +1,13 @@
import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button, Label } from 'react-bootstrap' import Icon from '@/shared/components/icon'
import Tooltip from '../../../shared/components/tooltip' import MaterialIcon from '@/shared/components/material-icon'
import Icon from '../../../shared/components/icon' import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import * as eventTracking from '@/infrastructure/event-tracking'
import * as eventTracking from '../../../infrastructure/event-tracking' import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLBadge from '@/features/ui/components/ol/ol-badge'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function PdfHybridLogsButton() { function PdfHybridLogsButton() {
const { error, logEntries, toggleLogs, showLogs, stoppedOnFirstError } = const { error, logEntries, toggleLogs, showLogs, stoppedOnFirstError } =
@ -25,13 +28,13 @@ function PdfHybridLogsButton() {
const totalCount = errorCount + warningCount const totalCount = errorCount + warningCount
return ( return (
<Tooltip <OLTooltip
id="logs-toggle" id="logs-toggle"
description={t('logs_and_output_files')} description={t('logs_and_output_files')}
overlayProps={{ placement: 'bottom' }} overlayProps={{ placement: 'bottom' }}
> >
<Button <OLButton
bsStyle="link" variant="link"
disabled={Boolean(error || stoppedOnFirstError)} disabled={Boolean(error || stoppedOnFirstError)}
active={showLogs} active={showLogs}
className="pdf-toolbar-btn toolbar-item log-btn" className="pdf-toolbar-btn toolbar-item log-btn"
@ -39,15 +42,18 @@ function PdfHybridLogsButton() {
style={{ position: 'relative' }} style={{ position: 'relative' }}
aria-label={showLogs ? t('view_pdf') : t('view_logs')} aria-label={showLogs ? t('view_pdf') : t('view_logs')}
> >
<Icon type="file-text-o" fw /> <BootstrapVersionSwitcher
bs3={<Icon type="file-text-o" fw />}
bs5={<MaterialIcon type="description" />}
/>
{!showLogs && totalCount > 0 && ( {!showLogs && totalCount > 0 && (
<Label bsStyle={errorCount === 0 ? 'warning' : 'danger'}> <OLBadge bg={errorCount === 0 ? 'warning' : 'danger'}>
{totalCount} {totalCount}
</Label> </OLBadge>
)} )}
</Button> </OLButton>
</Tooltip> </OLTooltip>
) )
} }

View file

@ -1,8 +1,8 @@
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import { buildUrlWithDetachRole } from '../../../shared/utils/url-helper' import { buildUrlWithDetachRole } from '@/shared/utils/url-helper'
import { useLocation } from '../../../shared/hooks/use-location' import { useLocation } from '@/shared/hooks/use-location'
import OLButton from '@/features/ui/components/ol/ol-button'
function PdfOrphanRefreshButton() { function PdfOrphanRefreshButton() {
const { t } = useTranslation() const { t } = useTranslation()
@ -13,14 +13,9 @@ function PdfOrphanRefreshButton() {
}, [location]) }, [location])
return ( return (
<Button <OLButton variant="primary" size="sm" onClick={redirect}>
onClick={redirect}
className="btn-orphan"
bsStyle="primary"
bsSize="small"
>
{t('redirect_to_editor')} {t('redirect_to_editor')}
</Button> </OLButton>
) )
} }

View file

@ -1,7 +1,7 @@
import { ButtonGroup } from 'react-bootstrap'
import PDFToolbarButton from './pdf-toolbar-button' import PDFToolbarButton from './pdf-toolbar-button'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import OLButtonGroup from '@/features/ui/components/ol/ol-button-group'
type PdfPageNumberControlProps = { type PdfPageNumberControlProps = {
setPage: (page: number) => void setPage: (page: number) => void
@ -38,7 +38,7 @@ function PdfPageNumberControl({
return ( return (
<> <>
<ButtonGroup className="pdfjs-toolbar-buttons "> <OLButtonGroup className="pdfjs-toolbar-buttons">
<PDFToolbarButton <PDFToolbarButton
tooltipId="pdf-controls-previous-page-tooltip" tooltipId="pdf-controls-previous-page-tooltip"
icon="keyboard_arrow_up" icon="keyboard_arrow_up"
@ -53,7 +53,7 @@ function PdfPageNumberControl({
disabled={page === totalPages} disabled={page === totalPages}
onClick={() => setPage(page + 1)} onClick={() => setPage(page + 1)}
/> />
</ButtonGroup> </OLButtonGroup>
<div className="pdfjs-page-number-input"> <div className="pdfjs-page-number-input">
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<input <input

View file

@ -1,7 +1,7 @@
import { memo, useState, useEffect, useRef } from 'react' import { memo, useState, useEffect, useRef } from 'react'
import { ButtonToolbar } from 'react-bootstrap' import OlButtonToolbar from '@/features/ui/components/ol/ol-button-toolbar'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useLayoutContext } from '../../../shared/context/layout-context' import { useLayoutContext } from '@/shared/context/layout-context'
import PdfCompileButton from './pdf-compile-button' import PdfCompileButton from './pdf-compile-button'
import SwitchToEditorButton from './switch-to-editor-button' import SwitchToEditorButton from './switch-to-editor-button'
import PdfHybridLogsButton from './pdf-hybrid-logs-button' import PdfHybridLogsButton from './pdf-hybrid-logs-button'
@ -10,6 +10,8 @@ import PdfHybridCodeCheckButton from './pdf-hybrid-code-check-button'
import PdfOrphanRefreshButton from './pdf-orphan-refresh-button' import PdfOrphanRefreshButton from './pdf-orphan-refresh-button'
import { DetachedSynctexControl } from './detach-synctex-control' import { DetachedSynctexControl } from './detach-synctex-control'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { Spinner } from 'react-bootstrap-5'
const ORPHAN_UI_TIMEOUT_MS = 5000 const ORPHAN_UI_TIMEOUT_MS = 5000
@ -47,9 +49,9 @@ function PdfPreviewHybridToolbar() {
} }
return ( return (
<ButtonToolbar className="toolbar toolbar-pdf toolbar-pdf-hybrid"> <OlButtonToolbar className="toolbar toolbar-pdf toolbar-pdf-hybrid">
{ToolbarInner} {ToolbarInner}
</ButtonToolbar> </OlButtonToolbar>
) )
} }
@ -88,7 +90,18 @@ function PdfPreviewHybridToolbarConnectingInner() {
return ( return (
<> <>
<div className="toolbar-pdf-orphan"> <div className="toolbar-pdf-orphan">
<Icon type="refresh" fw spin /> <BootstrapVersionSwitcher
bs3={<Icon type="refresh" fw spin />}
bs5={
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
}
/>
&nbsp;
{t('tab_connecting')} {t('tab_connecting')}
</div> </div>
</> </>

View file

@ -7,8 +7,6 @@ import { getJSON } from '../../../infrastructure/fetch-json'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { useLayoutContext } from '../../../shared/context/layout-context' import { useLayoutContext } from '../../../shared/context/layout-context'
import useScopeValue from '../../../shared/hooks/use-scope-value' import useScopeValue from '../../../shared/hooks/use-scope-value'
import { Button } from 'react-bootstrap'
import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useIsMounted from '../../../shared/hooks/use-is-mounted' import useIsMounted from '../../../shared/hooks/use-is-mounted'
@ -21,6 +19,12 @@ import useScopeEventListener from '../../../shared/hooks/use-scope-event-listene
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '../../../infrastructure/event-tracking'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path' import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
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 { bsVersion } from '@/features/utils/bootstrap-5'
function GoToCodeButton({ function GoToCodeButton({
position, position,
@ -30,15 +34,32 @@ function GoToCodeButton({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const tooltipPlacement = isDetachLayout ? 'bottom' : 'right' const tooltipPlacement = isDetachLayout ? 'bottom' : 'right'
const buttonClasses = classNames('synctex-control', 'btn-secondary', { const buttonClasses = classNames('synctex-control', {
'detach-synctex-control': !!isDetachLayout, 'detach-synctex-control': !!isDetachLayout,
}) })
let buttonIcon = null let buttonIcon = null
if (syncToCodeInFlight) { if (syncToCodeInFlight) {
buttonIcon = <Icon type="refresh" spin className="synctex-spin-icon" /> buttonIcon = (
<BootstrapVersionSwitcher
bs3={<Icon type="refresh" spin className="synctex-spin-icon" />}
bs5={
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
}
/>
)
} else if (!isDetachLayout) { } else if (!isDetachLayout) {
buttonIcon = <Icon type="arrow-left" className="synctex-control-icon" /> buttonIcon = (
<BootstrapVersionSwitcher
bs3={<Icon type="arrow-left" className="synctex-control-icon" />}
bs5={<MaterialIcon type="arrow_left_alt" />}
/>
)
} }
const syncToCodeWithButton = () => { const syncToCodeWithButton = () => {
@ -50,23 +71,26 @@ function GoToCodeButton({
} }
return ( return (
<Tooltip <OLTooltip
id="sync-to-code" id="sync-to-code"
description={t('go_to_pdf_location_in_code')} description={t('go_to_pdf_location_in_code')}
overlayProps={{ placement: tooltipPlacement }} overlayProps={{ placement: tooltipPlacement }}
> >
<Button <OLButton
bsStyle={null} variant="secondary"
bsSize="xs" size="sm"
onClick={syncToCodeWithButton} onClick={syncToCodeWithButton}
disabled={syncToCodeInFlight} disabled={syncToCodeInFlight}
className={buttonClasses} className={buttonClasses}
aria-label={t('go_to_pdf_location_in_code')} aria-label={t('go_to_pdf_location_in_code')}
bs3Props={{
bsSize: 'xs',
}}
> >
{buttonIcon} {buttonIcon}
{isDetachLayout ? <span>&nbsp;{t('show_in_code')}</span> : ''} {isDetachLayout ? <span>&nbsp;{t('show_in_code')}</span> : ''}
</Button> </OLButton>
</Tooltip> </OLTooltip>
) )
} }
@ -81,8 +105,7 @@ function GoToPdfButton({
const tooltipPlacement = isDetachLayout ? 'bottom' : 'right' const tooltipPlacement = isDetachLayout ? 'bottom' : 'right'
const buttonClasses = classNames( const buttonClasses = classNames(
'synctex-control', 'synctex-control',
'btn-secondary', bsVersion({ bs3: 'toolbar-btn-secondary' }),
'toolbar-btn-secondary',
{ {
'detach-synctex-control': !!isDetachLayout, 'detach-synctex-control': !!isDetachLayout,
} }
@ -90,29 +113,49 @@ function GoToPdfButton({
let buttonIcon = null let buttonIcon = null
if (syncToPdfInFlight) { if (syncToPdfInFlight) {
buttonIcon = <Icon type="refresh" spin className="synctex-spin-icon" /> buttonIcon = (
<BootstrapVersionSwitcher
bs3={<Icon type="refresh" spin className="synctex-spin-icon" />}
bs5={
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
}
/>
)
} else if (!isDetachLayout) { } else if (!isDetachLayout) {
buttonIcon = <Icon type="arrow-right" className="synctex-control-icon" /> buttonIcon = (
<BootstrapVersionSwitcher
bs3={<Icon type="arrow-right" className="synctex-control-icon" />}
bs5={<MaterialIcon type="arrow_right_alt" />}
/>
)
} }
return ( return (
<Tooltip <OLTooltip
id="sync-to-pdf" id="sync-to-pdf"
description={t('go_to_code_location_in_pdf')} description={t('go_to_code_location_in_pdf')}
overlayProps={{ placement: tooltipPlacement }} overlayProps={{ placement: tooltipPlacement }}
> >
<Button <OLButton
bsStyle={null} variant="secondary"
bsSize="xs" size="sm"
onClick={() => syncToPdf(cursorPosition)} onClick={() => syncToPdf(cursorPosition)}
disabled={syncToPdfInFlight || !cursorPosition || !hasSingleSelectedDoc} disabled={syncToPdfInFlight || !cursorPosition || !hasSingleSelectedDoc}
className={buttonClasses} className={buttonClasses}
aria-label={t('go_to_code_location_in_pdf')} aria-label={t('go_to_code_location_in_pdf')}
bs3Props={{
bsSize: 'xs',
}}
> >
{buttonIcon} {buttonIcon}
{isDetachLayout ? <span>&nbsp;{t('show_in_pdf')}</span> : ''} {isDetachLayout ? <span>&nbsp;{t('show_in_pdf')}</span> : ''}
</Button> </OLButton>
</Tooltip> </OLTooltip>
) )
} }

View file

@ -1,6 +1,6 @@
import Button from 'react-bootstrap/lib/Button'
import MaterialIcon from '@/shared/components/material-icon' import MaterialIcon from '@/shared/components/material-icon'
import Tooltip from '@/shared/components/tooltip' import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLButton from '@/features/ui/components/ol/ol-button'
type PDFToolbarButtonProps = { type PDFToolbarButtonProps = {
tooltipId: string tooltipId: string
@ -20,7 +20,7 @@ export default function PDFToolbarButton({
shortcut, shortcut,
}: PDFToolbarButtonProps) { }: PDFToolbarButtonProps) {
return ( return (
<Tooltip <OLTooltip
id={tooltipId} id={tooltipId}
description={ description={
<> <>
@ -30,16 +30,15 @@ export default function PDFToolbarButton({
} }
overlayProps={{ placement: 'bottom' }} overlayProps={{ placement: 'bottom' }}
> >
<Button <OLButton
aria-label={label} variant="ghost"
bsSize="large"
bsStyle={null}
className="pdf-toolbar-btn pdfjs-toolbar-button" className="pdf-toolbar-btn pdfjs-toolbar-button"
disabled={disabled} disabled={disabled}
onClick={onClick} onClick={onClick}
aria-label={label}
> >
<MaterialIcon type={icon} /> <MaterialIcon type={icon} />
</Button> </OLButton>
</Tooltip> </OLTooltip>
) )
} }

View file

@ -2,11 +2,13 @@ import { useRef } from 'react'
import PdfPageNumberControl from './pdf-page-number-control' import PdfPageNumberControl from './pdf-page-number-control'
import PdfZoomButtons from './pdf-zoom-buttons' import PdfZoomButtons from './pdf-zoom-buttons'
import { Button, Overlay, Popover } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon' import MaterialIcon from '@/shared/components/material-icon'
import Tooltip from '@/shared/components/tooltip'
import useDropdown from '@/shared/hooks/use-dropdown' import useDropdown from '@/shared/hooks/use-dropdown'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
import OLPopover from '@/features/ui/components/ol/ol-popover'
type PdfViewerControlsMenuButtonProps = { type PdfViewerControlsMenuButtonProps = {
setZoom: (zoom: string) => void setZoom: (zoom: string) => void
@ -29,34 +31,37 @@ export default function PdfViewerControlsMenuButton({
ref: popoverRef, ref: popoverRef,
} = useDropdown() } = useDropdown()
const targetRef = useRef<any>(null) const targetRef = useRef<HTMLButtonElement | null>(null)
return ( return (
<> <>
<Tooltip <OLTooltip
id="pdf-controls-menu-tooltip" id="pdf-controls-menu-tooltip"
description={t('view_options')} description={t('view_options')}
overlayProps={{ placement: 'bottom' }} overlayProps={{ placement: 'bottom' }}
> >
<Button <span>
className="pdf-toolbar-btn pdfjs-toolbar-popover-button" <OLButton
onClick={togglePopover} variant="ghost"
ref={targetRef} className="pdf-toolbar-btn pdfjs-toolbar-popover-button"
> onClick={togglePopover}
<MaterialIcon type="more_horiz" /> ref={targetRef}
</Button> >
</Tooltip> <MaterialIcon type="more_horiz" />
</OLButton>
</span>
</OLTooltip>
<Overlay <OLOverlay
show={popoverOpen} show={popoverOpen}
target={targetRef.current} target={targetRef.current}
placement="bottom" placement="bottom"
containerPadding={0} containerPadding={0}
animation transition
rootClose rootClose
onHide={() => togglePopover(false)} onHide={() => togglePopover(false)}
> >
<Popover <OLPopover
className="pdfjs-toolbar-popover" className="pdfjs-toolbar-popover"
id="pdf-toolbar-popover-menu" id="pdf-toolbar-popover-menu"
ref={popoverRef} ref={popoverRef}
@ -69,8 +74,8 @@ export default function PdfViewerControlsMenuButton({
<div className="pdfjs-zoom-controls"> <div className="pdfjs-zoom-controls">
<PdfZoomButtons setZoom={setZoom} /> <PdfZoomButtons setZoom={setZoom} />
</div> </div>
</Popover> </OLPopover>
</Overlay> </OLOverlay>
</> </>
) )
} }

View file

@ -1,6 +1,6 @@
import { ButtonGroup } from 'react-bootstrap'
import PDFToolbarButton from './pdf-toolbar-button' import PDFToolbarButton from './pdf-toolbar-button'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import OLButtonGroup from '@/features/ui/components/ol/ol-button-group'
const isMac = /Mac/.test(window.navigator?.platform) const isMac = /Mac/.test(window.navigator?.platform)
@ -15,7 +15,7 @@ function PdfZoomButtons({ setZoom }: PdfZoomButtonsProps) {
const zoomOutShortcut = isMac ? '⌘-' : 'Ctrl+-' const zoomOutShortcut = isMac ? '⌘-' : 'Ctrl+-'
return ( return (
<ButtonGroup className="pdfjs-toolbar-buttons"> <OLButtonGroup className="pdfjs-toolbar-buttons">
<PDFToolbarButton <PDFToolbarButton
tooltipId="pdf-controls-zoom-out-tooltip" tooltipId="pdf-controls-zoom-out-tooltip"
label={t('zoom_out')} label={t('zoom_out')}
@ -30,7 +30,7 @@ function PdfZoomButtons({ setZoom }: PdfZoomButtonsProps) {
onClick={() => setZoom('zoom-in')} onClick={() => setZoom('zoom-in')}
shortcut={zoomInShortcut} shortcut={zoomInShortcut}
/> />
</ButtonGroup> </OLButtonGroup>
) )
} }

View file

@ -1,9 +1,19 @@
import { Dropdown, MenuItem } from 'react-bootstrap' import { Dropdown as BS3Dropdown, MenuItem } from 'react-bootstrap'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ControlledDropdown from '@/shared/components/controlled-dropdown' import ControlledDropdown from '@/shared/components/controlled-dropdown'
import classNames from 'classnames' import classNames from 'classnames'
import { useFeatureFlag } from '@/shared/context/split-test-context' import { useFeatureFlag } from '@/shared/context/split-test-context'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import {
Dropdown,
DropdownDivider,
DropdownHeader,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
const isMac = /Mac/.test(window.navigator?.platform) const isMac = /Mac/.test(window.navigator?.platform)
@ -53,93 +63,215 @@ function PdfZoomDropdown({
const showPresentOption = enablePresentationMode && document.fullscreenEnabled const showPresentOption = enablePresentationMode && document.fullscreenEnabled
return ( return (
<ControlledDropdown <BootstrapVersionSwitcher
id="pdf-zoom-dropdown" bs3={
onSelect={eventKey => { <ControlledDropdown
if (eventKey === 'custom-zoom') { id="pdf-zoom-dropdown"
return onSelect={eventKey => {
} if (eventKey === 'custom-zoom') {
return
}
if (eventKey === 'present') { if (eventKey === 'present') {
requestPresentationMode() requestPresentationMode()
return return
} }
setZoom(eventKey) setZoom(eventKey)
}} }}
pullRight pullRight
>
<Dropdown.Toggle
bsStyle={null}
className="btn pdf-toolbar-btn pdfjs-zoom-dropdown-button small"
value={rawScale}
title={rawScaleToPercentage(rawScale)}
/>
<Dropdown.Menu className="pdfjs-zoom-dropdown-menu">
<MenuItem
draggable={false}
disabled
className="pdfjs-custom-zoom-menu-item"
key="custom-zoom"
eventKey="custom-zoom"
> >
<input <BS3Dropdown.Toggle
type="text" bsStyle={null}
onFocus={event => event.target.select()} className="btn pdf-toolbar-btn pdfjs-zoom-dropdown-button small"
value={customZoomValue} value={rawScale}
onKeyDown={event => { title={rawScaleToPercentage(rawScale)}
if (event.key === 'Enter') {
const zoom = Number(customZoomValue.replace('%', '')) / 100
// Only allow zoom values between 10% and 999%
if (zoom < 0.1) {
setZoom('0.1')
} else if (zoom > 9.99) {
setZoom('9.99')
} else {
setZoom(`${zoom}`)
}
}
}}
onChange={event => {
const rawValue = event.target.value
const parsedValue = rawValue.replace(/[^0-9%]/g, '')
setCustomZoomValue(parsedValue)
}}
/> />
</MenuItem> <BS3Dropdown.Menu className="pdfjs-zoom-dropdown-menu">
<MenuItem divider /> <MenuItem
<MenuItem draggable={false} key="zoom-in" eventKey="zoom-in"> draggable={false}
<span>{t('zoom_in')}</span> disabled
<Shortcut keys={shortcuts['zoom-in']} /> className="pdfjs-custom-zoom-menu-item"
</MenuItem> key="custom-zoom"
<MenuItem draggable={false} key="zoom-out" eventKey="zoom-out"> eventKey="custom-zoom"
<span>{t('zoom_out')}</span> >
<Shortcut keys={shortcuts['zoom-out']} /> <input
</MenuItem> type="text"
<MenuItem draggable={false} key="page-width" eventKey="page-width"> onFocus={event => event.target.select()}
{t('fit_to_width')} value={customZoomValue}
<Shortcut keys={shortcuts['fit-to-width']} /> onKeyDown={event => {
</MenuItem> if (event.key === 'Enter') {
<MenuItem draggable={false} key="page-height" eventKey="page-height"> const zoom = Number(customZoomValue.replace('%', '')) / 100
{t('fit_to_height')}
<Shortcut keys={shortcuts['fit-to-height']} /> // Only allow zoom values between 10% and 999%
</MenuItem> if (zoom < 0.1) {
{showPresentOption && <MenuItem divider />} setZoom('0.1')
{showPresentOption && ( } else if (zoom > 9.99) {
<MenuItem draggable={false} key="present" eventKey="present"> setZoom('9.99')
{t('presentation_mode')} } else {
</MenuItem> setZoom(`${zoom}`)
)} }
<MenuItem divider /> }
<MenuItem header>{t('zoom_to')}</MenuItem> }}
{zoomValues.map(value => ( onChange={event => {
<MenuItem draggable={false} key={value} eventKey={value}> const rawValue = event.target.value
{rawScaleToPercentage(Number(value))} const parsedValue = rawValue.replace(/[^0-9%]/g, '')
</MenuItem> setCustomZoomValue(parsedValue)
))} }}
</Dropdown.Menu> />
</ControlledDropdown> </MenuItem>
<MenuItem divider />
<MenuItem draggable={false} key="zoom-in" eventKey="zoom-in">
<span>{t('zoom_in')}</span>
<Shortcut keys={shortcuts['zoom-in']} />
</MenuItem>
<MenuItem draggable={false} key="zoom-out" eventKey="zoom-out">
<span>{t('zoom_out')}</span>
<Shortcut keys={shortcuts['zoom-out']} />
</MenuItem>
<MenuItem draggable={false} key="page-width" eventKey="page-width">
{t('fit_to_width')}
<Shortcut keys={shortcuts['fit-to-width']} />
</MenuItem>
<MenuItem
draggable={false}
key="page-height"
eventKey="page-height"
>
{t('fit_to_height')}
<Shortcut keys={shortcuts['fit-to-height']} />
</MenuItem>
{showPresentOption && <MenuItem divider />}
{showPresentOption && (
<MenuItem draggable={false} key="present" eventKey="present">
{t('presentation_mode')}
</MenuItem>
)}
<MenuItem divider />
<MenuItem header>{t('zoom_to')}</MenuItem>
{zoomValues.map(value => (
<MenuItem draggable={false} key={value} eventKey={value}>
{rawScaleToPercentage(Number(value))}
</MenuItem>
))}
</BS3Dropdown.Menu>
</ControlledDropdown>
}
bs5={
<Dropdown
onSelect={eventKey => {
if (eventKey === 'custom-zoom') {
return
}
if (eventKey === 'present') {
requestPresentationMode()
return
}
setZoom(eventKey)
}}
align="end"
>
<DropdownToggle
id="pdf-zoom-dropdown"
variant="link"
className="pdf-toolbar-btn pdfjs-zoom-dropdown-button"
>
<small>{rawScaleToPercentage(rawScale)}</small>
</DropdownToggle>
<DropdownMenu className="pdfjs-zoom-dropdown-menu">
<li role="none">
<DropdownItem
disabled
as="div"
className="pdfjs-custom-zoom-menu-item"
eventKey="custom-zoom"
>
<FormControl
onFocus={event => event.target.select()}
value={customZoomValue}
onKeyDown={event => {
if (event.key === 'Enter') {
const zoom =
Number(customZoomValue.replace('%', '')) / 100
// Only allow zoom values between 10% and 999%
if (zoom < 0.1) {
setZoom('0.1')
} else if (zoom > 9.99) {
setZoom('9.99')
} else {
setZoom(`${zoom}`)
}
}
}}
onChange={event => {
const rawValue = event.target.value
const parsedValue = rawValue.replace(/[^0-9%]/g, '')
setCustomZoomValue(parsedValue)
}}
/>
</DropdownItem>
</li>
<DropdownDivider />
<li role="none">
<DropdownItem
as="button"
eventKey="zoom-in"
trailingIcon={<Shortcut keys={shortcuts['zoom-in']} />}
>
{t('zoom_in')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
eventKey="zoom-out"
trailingIcon={<Shortcut keys={shortcuts['zoom-out']} />}
>
{t('zoom_out')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
eventKey="page-width"
trailingIcon={<Shortcut keys={shortcuts['fit-to-width']} />}
>
{t('fit_to_width')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
eventKey="page-height"
trailingIcon={<Shortcut keys={shortcuts['fit-to-height']} />}
>
{t('fit_to_height')}
</DropdownItem>
</li>
{showPresentOption && <DropdownDivider />}
{showPresentOption && (
<li role="none">
<DropdownItem as="button" eventKey="present">
{t('presentation_mode')}
</DropdownItem>
</li>
)}
<DropdownDivider />
<DropdownHeader aria-hidden="true">{t('zoom_to')}</DropdownHeader>
{zoomValues.map(value => (
<li role="none" key={value}>
<DropdownItem as="button" eventKey={value}>
{rawScaleToPercentage(Number(value))}
</DropdownItem>
</li>
))}
</DropdownMenu>
</Dropdown>
}
/>
) )
} }

View file

@ -25,7 +25,7 @@ function SwitchToEditorButton() {
return ( return (
<OLButton <OLButton
variant="secondary" variant="secondary"
size="small" size="sm"
onClick={handleClick} onClick={handleClick}
bs3Props={{ bs3Props={{
bsSize: 'xsmall', bsSize: 'xsmall',

View file

@ -12,7 +12,7 @@ import DeleteLeaveProjectsButton from './buttons/delete-leave-projects-button'
import LeaveProjectsButton from './buttons/leave-projects-button' import LeaveProjectsButton from './buttons/leave-projects-button'
import DeleteProjectsButton from './buttons/delete-projects-button' import DeleteProjectsButton from './buttons/delete-projects-button'
import OLButtonToolbar from '@/features/ui/components/ol/ol-button-toolbar' import OLButtonToolbar from '@/features/ui/components/ol/ol-button-toolbar'
import OlButtonGroup from '@/features/ui/components/ol/ol-button-group' import OLButtonGroup from '@/features/ui/components/ol/ol-button-group'
function ProjectTools() { function ProjectTools() {
const { t } = useTranslation() const { t } = useTranslation()
@ -20,27 +20,27 @@ function ProjectTools() {
return ( return (
<OLButtonToolbar aria-label={t('toolbar_selected_projects')}> <OLButtonToolbar aria-label={t('toolbar_selected_projects')}>
<OlButtonGroup <OLButtonGroup
aria-label={t('toolbar_selected_projects_management_actions')} aria-label={t('toolbar_selected_projects_management_actions')}
> >
<DownloadProjectsButton /> <DownloadProjectsButton />
{filter !== 'archived' && <ArchiveProjectsButton />} {filter !== 'archived' && <ArchiveProjectsButton />}
{filter !== 'trashed' && <TrashProjectsButton />} {filter !== 'trashed' && <TrashProjectsButton />}
</OlButtonGroup> </OLButtonGroup>
{(filter === 'trashed' || filter === 'archived') && ( {(filter === 'trashed' || filter === 'archived') && (
<OlButtonGroup aria-label={t('toolbar_selected_projects_restore')}> <OLButtonGroup aria-label={t('toolbar_selected_projects_restore')}>
{filter === 'trashed' && <UntrashProjectsButton />} {filter === 'trashed' && <UntrashProjectsButton />}
{filter === 'archived' && <UnarchiveProjectsButton />} {filter === 'archived' && <UnarchiveProjectsButton />}
</OlButtonGroup> </OLButtonGroup>
)} )}
{filter === 'trashed' && ( {filter === 'trashed' && (
<OlButtonGroup aria-label={t('toolbar_selected_projects_remove')}> <OLButtonGroup aria-label={t('toolbar_selected_projects_remove')}>
<LeaveProjectsButton /> <LeaveProjectsButton />
<DeleteProjectsButton /> <DeleteProjectsButton />
<DeleteLeaveProjectsButton /> <DeleteLeaveProjectsButton />
</OlButtonGroup> </OLButtonGroup>
)} )}
{!['archived', 'trashed'].includes(filter) && <TagsDropdown />} {!['archived', 'trashed'].includes(filter) && <TagsDropdown />}

View file

@ -8,7 +8,7 @@ function PrimaryButton({
}: OLButtonProps) { }: OLButtonProps) {
return ( return (
<OLButton <OLButton
size="small" size="sm"
disabled={disabled && !isLoading} disabled={disabled && !isLoading}
isLoading={isLoading} isLoading={isLoading}
onClick={onClick} onClick={onClick}

View file

@ -23,7 +23,7 @@ function DeleteButton({ disabled, isLoading, onClick }: DeleteButtonProps) {
variant="danger" variant="danger"
disabled={disabled} disabled={disabled}
isLoading={isLoading} isLoading={isLoading}
size="small" size="sm"
onClick={onClick} onClick={onClick}
accessibilityLabel={t('remove') || ''} accessibilityLabel={t('remove') || ''}
icon={ icon={

View file

@ -56,7 +56,7 @@ function SsoLinkingInfo({ domainInfo, email }: SSOLinkingInfoProps) {
<OLButton <OLButton
variant="primary" variant="primary"
className="btn-link-accounts" className="btn-link-accounts"
size="small" size="sm"
disabled={linkAccountsButtonDisabled} disabled={linkAccountsButtonDisabled}
onClick={handleLinkAccountsButtonClick} onClick={handleLinkAccountsButtonClick}
> >

View file

@ -152,7 +152,7 @@ function SSOAffiliationInfo({ userEmailData }: SSOAffiliationInfoProps) {
className="btn-link-accounts" className="btn-link-accounts"
disabled={linkAccountsButtonDisabled} disabled={linkAccountsButtonDisabled}
onClick={handleLinkAccountsButtonClick} onClick={handleLinkAccountsButtonClick}
size="small" size="sm"
> >
{t('link_accounts')} {t('link_accounts')}
</OLButton> </OLButton>

View file

@ -43,7 +43,7 @@ export default function AccessLevelsChanged({
<div className="upgrade-actions"> <div className="upgrade-actions">
{user.allowedFreeTrial ? ( {user.allowedFreeTrial ? (
<StartFreeTrialButton <StartFreeTrialButton
buttonProps={{ variant: 'secondary', size: 'small' }} buttonProps={{ variant: 'secondary', size: 'sm' }}
source="project-sharing" source="project-sharing"
variant="exceeds" variant="exceeds"
> >

View file

@ -33,7 +33,7 @@ export default function AddCollaboratorsUpgrade() {
<div className="upgrade-actions"> <div className="upgrade-actions">
{user.allowedFreeTrial ? ( {user.allowedFreeTrial ? (
<StartFreeTrialButton <StartFreeTrialButton
buttonProps={{ variant: 'secondary', size: 'small' }} buttonProps={{ variant: 'secondary', size: 'sm' }}
source="project-sharing" source="project-sharing"
variant="exceeds" variant="exceeds"
> >

View file

@ -37,7 +37,7 @@ export default function CollaboratorsLimitUpgrade() {
action={ action={
user.allowedFreeTrial ? ( user.allowedFreeTrial ? (
<StartFreeTrialButton <StartFreeTrialButton
buttonProps={{ variant: 'premium', size: 'default' }} buttonProps={{ variant: 'premium' }}
source="project-sharing" source="project-sharing"
variant="limit" variant="limit"
> >
@ -71,7 +71,7 @@ export default function CollaboratorsLimitUpgrade() {
action={ action={
user.allowedFreeTrial ? ( user.allowedFreeTrial ? (
<StartFreeTrialButton <StartFreeTrialButton
buttonProps={{ variant: 'secondary', size: 'small' }} buttonProps={{ variant: 'secondary', size: 'sm' }}
source="project-sharing" source="project-sharing"
variant="limit" variant="limit"
> >

View file

@ -25,7 +25,7 @@ function SwitchToPDFButton() {
return ( return (
<OLButton <OLButton
variant="secondary" variant="secondary"
size="small" size="sm"
onClick={handleClick} onClick={handleClick}
bs3Props={{ bs3Props={{
bsSize: 'xsmall', bsSize: 'xsmall',

View file

@ -345,7 +345,7 @@ export function ChangeToGroupModal() {
)} )}
<OLButton <OLButton
variant="primary" variant="primary"
size="large" size="lg"
disabled={ disabled={
queryingGroupPlanToChangeToPrice || queryingGroupPlanToChangeToPrice ||
!groupPlanToChangeToPrice || !groupPlanToChangeToPrice ||

View file

@ -1,14 +1,14 @@
import { Badge as BSBadge } from 'react-bootstrap-5' import { Badge as BSBadge, BadgeProps as BSBadgeProps } from 'react-bootstrap-5'
import { MergeAndOverride } from '../../../../../../types/utils' import { MergeAndOverride } from '../../../../../../types/utils'
type BadgeProps = MergeAndOverride< type BadgeProps = MergeAndOverride<
React.ComponentProps<typeof BSBadge>, BSBadgeProps,
{ {
prepend?: React.ReactNode prepend?: React.ReactNode
} }
> >
function Badge({ prepend, children, closeBtnProps, ...rest }: BadgeProps) { function Badge({ prepend, children, ...rest }: BadgeProps) {
return ( return (
<BSBadge {...rest}> <BSBadge {...rest}>
{prepend && <span className="badge-prepend">{prepend}</span>} {prepend && <span className="badge-prepend">{prepend}</span>}

View file

@ -5,12 +5,6 @@ import classNames from 'classnames'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon' import MaterialIcon from '@/shared/components/material-icon'
const sizeClasses = new Map<ButtonProps['size'], string>([
['small', 'btn-sm'],
['default', ''],
['large', 'btn-lg'],
])
const Button = forwardRef<HTMLButtonElement, ButtonProps>( const Button = forwardRef<HTMLButtonElement, ButtonProps>(
( (
{ {
@ -19,7 +13,6 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
leadingIcon, leadingIcon,
isLoading = false, isLoading = false,
loadingLabel, loadingLabel,
size = 'default',
trailingIcon, trailingIcon,
variant = 'primary', variant = 'primary',
...props ...props
@ -28,13 +21,28 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
) => { ) => {
const { t } = useTranslation() const { t } = useTranslation()
const sizeClass = sizeClasses.get(size) const buttonClassName = classNames('d-inline-grid', className, {
const buttonClassName = classNames('d-inline-grid', sizeClass, className, {
'button-loading': isLoading, 'button-loading': isLoading,
}) })
const loadingSpinnerClassName = const loadingSpinnerClassName =
size === 'large' ? 'loading-spinner-large' : 'loading-spinner-small' props.size === 'lg' ? 'loading-spinner-large' : 'loading-spinner-small'
const materialIconClassName = size === 'large' ? 'icon-large' : 'icon-small' const materialIconClassName =
props.size === 'lg' ? 'icon-large' : 'icon-small'
const leadingIconComponent =
leadingIcon && typeof leadingIcon === 'string' ? (
<MaterialIcon type={leadingIcon} className={materialIconClassName} />
) : (
leadingIcon
)
const trailingIconComponent =
trailingIcon && typeof trailingIcon === 'string' ? (
<MaterialIcon type={trailingIcon} className={materialIconClassName} />
) : (
trailingIcon
)
return ( return (
<BS5Button <BS5Button
@ -58,19 +66,9 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
</span> </span>
)} )}
<span className="button-content" aria-hidden={isLoading}> <span className="button-content" aria-hidden={isLoading}>
{leadingIcon && ( {leadingIconComponent}
<MaterialIcon
type={leadingIcon}
className={materialIconClassName}
/>
)}
{children} {children}
{trailingIcon && ( {trailingIconComponent}
<MaterialIcon
type={trailingIcon}
className={materialIconClassName}
/>
)}
</span> </span>
</BS5Button> </BS5Button>
) )

View file

@ -1,51 +0,0 @@
import classNames from 'classnames'
import Button from './button'
import {
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from './dropdown-menu'
import MaterialIcon from '@/shared/components/material-icon'
import type { SplitButtonProps } from '@/features/ui/components/types/split-button-props'
export function SplitButton({
accessibilityLabel,
align,
id,
items,
text,
variant,
...props
}: SplitButtonProps) {
const buttonClassName = classNames('split-button')
return (
<div>
<Dropdown align={align}>
<Button className={buttonClassName} variant={variant} {...props}>
<span className="split-button-content">{text}</span>
</Button>
<DropdownToggle
bsPrefix="dropdown-button-toggle"
id={id}
variant={variant}
{...props}
>
<MaterialIcon
className="split-button-caret"
type="expand_more"
accessibilityLabel={accessibilityLabel}
/>
</DropdownToggle>
<DropdownMenu>
{items.map((item, index) => (
<li key={index}>
<DropdownItem eventKey={item.eventKey}>{item.label}</DropdownItem>
</li>
))}
</DropdownMenu>
</Dropdown>
</div>
)
}

View file

@ -10,7 +10,7 @@ type OLButtonGroupProps = ButtonGroupProps & {
bs3Props?: Record<string, unknown> bs3Props?: Record<string, unknown>
} }
function OlButtonGroup({ bs3Props, as, ...rest }: OLButtonGroupProps) { function OLButtonGroup({ bs3Props, as, ...rest }: OLButtonGroupProps) {
const bs3ButtonGroupProps: BS3ButtonGroupProps = { const bs3ButtonGroupProps: BS3ButtonGroupProps = {
children: rest.children, children: rest.children,
className: rest.className, className: rest.className,
@ -27,4 +27,4 @@ function OlButtonGroup({ bs3Props, as, ...rest }: OLButtonGroupProps) {
) )
} }
export default OlButtonGroup export default OLButtonGroup

View file

@ -1,3 +1,4 @@
import { forwardRef } from 'react'
import BootstrapVersionSwitcher from '../bootstrap-5/bootstrap-version-switcher' import BootstrapVersionSwitcher from '../bootstrap-5/bootstrap-version-switcher'
import { Button as BS3Button } from 'react-bootstrap' import { Button as BS3Button } from 'react-bootstrap'
import type { ButtonProps } from '@/features/ui/components/types/button-props' import type { ButtonProps } from '@/features/ui/components/types/button-props'
@ -5,7 +6,7 @@ import type { ButtonProps as BS3ButtonPropsBase } from 'react-bootstrap'
import Button from '../bootstrap-5/button' import Button from '../bootstrap-5/button'
import classnames from 'classnames' import classnames from 'classnames'
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
import { callFnsInSequence } from '@/utils/functions'
export type BS3ButtonSize = 'xsmall' | 'sm' | 'medium' | 'lg' export type BS3ButtonSize = 'xsmall' | 'sm' | 'medium' | 'lg'
export type OLButtonProps = ButtonProps & { export type OLButtonProps = ButtonProps & {
@ -14,6 +15,10 @@ export type OLButtonProps = ButtonProps & {
bsSize?: BS3ButtonSize bsSize?: BS3ButtonSize
block?: boolean block?: boolean
className?: string className?: string
onMouseOver?: React.MouseEventHandler<HTMLButtonElement>
onMouseOut?: React.MouseEventHandler<HTMLButtonElement>
onFocus?: React.FocusEventHandler<HTMLButtonElement>
onBlur?: React.FocusEventHandler<HTMLButtonElement>
} }
} }
@ -25,7 +30,7 @@ export type BS3ButtonProps = Omit<BS3ButtonPropsBase, 'onClick'> & {
export function bs3ButtonProps(props: ButtonProps) { export function bs3ButtonProps(props: ButtonProps) {
const bs3ButtonProps: BS3ButtonProps = { const bs3ButtonProps: BS3ButtonProps = {
bsStyle: null, bsStyle: null,
bsSize: mapBsButtonSizes(props.size), bsSize: props.size,
className: classnames(`btn-${props.variant || 'primary'}`, props.className), className: classnames(`btn-${props.variant || 'primary'}`, props.className),
disabled: props.isLoading || props.disabled, disabled: props.isLoading || props.disabled,
form: props.form, form: props.form,
@ -34,38 +39,51 @@ export function bs3ButtonProps(props: ButtonProps) {
rel: props.rel, rel: props.rel,
onClick: props.onClick, onClick: props.onClick,
type: props.type, type: props.type,
draggable: props.draggable,
download: props.download,
style: props.style,
active: props.active,
} }
return bs3ButtonProps return bs3ButtonProps
} }
// maps Bootstrap 5 sizes to Bootstrap 3 sizes const OLButton = forwardRef<HTMLButtonElement, OLButtonProps>(
export const mapBsButtonSizes = ( ({ bs3Props = {}, ...rest }, ref) => {
size: ButtonProps['size'] const { className: _, ...restBs3Props } = bs3Props
): 'sm' | 'lg' | undefined =>
size === 'small' ? 'sm' : size === 'large' ? 'lg' : undefined
export default function OLButton({ bs3Props = {}, ...rest }: OLButtonProps) { // BS3 OverlayTrigger automatically provides 'onMouseOver', 'onMouseOut', 'onFocus', 'onBlur' event handlers
const { className: _, ...restBs3Props } = bs3Props const bs3FinalProps = {
...restBs3Props,
onMouseOver: callFnsInSequence(bs3Props?.onMouseOver, rest.onMouseOver),
onMouseOut: callFnsInSequence(bs3Props?.onMouseOut, rest.onMouseOut),
onFocus: callFnsInSequence(bs3Props?.onFocus, rest.onFocus),
onBlur: callFnsInSequence(bs3Props?.onBlur, rest.onBlur),
}
// Get all `aria-*` and `data-*` attributes // Get all `aria-*` and `data-*` attributes
const extraProps = getAriaAndDataProps(rest) const extraProps = getAriaAndDataProps(rest)
return ( return (
<BootstrapVersionSwitcher <BootstrapVersionSwitcher
bs3={ bs3={
<BS3Button <BS3Button
{...bs3ButtonProps({ {...bs3ButtonProps({
...rest, ...rest,
// Override the `className` with bs3 specific className (if provided) // Override the `className` with bs3 specific className (if provided)
className: bs3Props?.className ?? rest.className, className: bs3Props?.className ?? rest.className,
})} })}
{...restBs3Props} {...extraProps}
{...extraProps} {...bs3FinalProps}
> ref={ref as React.LegacyRef<any> | undefined}
{bs3Props?.loading || rest.children} >
</BS3Button> {bs3Props?.loading || rest.children}
} </BS3Button>
bs5={<Button {...rest} />} }
/> bs5={<Button {...rest} ref={ref} />}
) />
} )
}
)
OLButton.displayName = 'OLButton'
export default OLButton

View file

@ -9,10 +9,13 @@ type OLTooltipProps = React.ComponentProps<typeof Tooltip> & {
function OLTooltip(props: OLTooltipProps) { function OLTooltip(props: OLTooltipProps) {
const { bs3Props, ...bs5Props } = props const { bs3Props, ...bs5Props } = props
const bs3TooltipProps: React.ComponentProps<typeof BS3Tooltip> = { type BS3TooltipProps = React.ComponentProps<typeof BS3Tooltip>
const bs3TooltipProps: BS3TooltipProps = {
children: bs5Props.children, children: bs5Props.children,
id: bs5Props.id, id: bs5Props.id,
description: bs5Props.description, description: bs5Props.description,
tooltipProps: bs5Props.tooltipProps as BS3TooltipProps,
overlayProps: { overlayProps: {
placement: bs5Props.overlayProps?.placement, placement: bs5Props.overlayProps?.placement,
}, },

View file

@ -4,8 +4,10 @@ export type ButtonProps = {
children?: ReactNode children?: ReactNode
className?: string className?: string
disabled?: boolean disabled?: boolean
download?: boolean
draggable?: boolean
form?: string form?: string
leadingIcon?: string leadingIcon?: string | React.ReactNode
href?: string href?: string
target?: string target?: string
rel?: string rel?: string
@ -16,8 +18,10 @@ export type ButtonProps = {
onMouseOut?: React.MouseEventHandler<HTMLButtonElement> onMouseOut?: React.MouseEventHandler<HTMLButtonElement>
onFocus?: React.FocusEventHandler<HTMLButtonElement> onFocus?: React.FocusEventHandler<HTMLButtonElement>
onBlur?: React.FocusEventHandler<HTMLButtonElement> onBlur?: React.FocusEventHandler<HTMLButtonElement>
size?: 'small' | 'default' | 'large' size?: 'sm' | 'lg' | undefined
trailingIcon?: string style?: Record<PropertyKey, string>
active?: boolean
trailingIcon?: string | React.ReactNode
type?: 'button' | 'reset' | 'submit' type?: 'button' | 'reset' | 'submit'
variant?: variant?:
| 'primary' | 'primary'

View file

@ -1,5 +1,10 @@
import type { ElementType, ReactNode, PropsWithChildren } from 'react' import type { ElementType, ReactNode, PropsWithChildren } from 'react'
import type { SplitButtonVariants } from './split-button-props' import type { ButtonProps } from '@/features/ui/components/types/button-props'
type SplitButtonVariants = Extract<
ButtonProps['variant'],
'primary' | 'secondary' | 'danger' | 'link'
>
export type DropdownProps = { export type DropdownProps = {
align?: align?:
@ -47,7 +52,7 @@ export type DropdownToggleProps = PropsWithChildren<{
id?: string // necessary for assistive technologies id?: string // necessary for assistive technologies
variant?: SplitButtonVariants variant?: SplitButtonVariants
as?: ElementType as?: ElementType
size?: 'sm' | 'lg' size?: 'sm' | 'lg' | undefined
}> }>
export type DropdownMenuProps = PropsWithChildren<{ export type DropdownMenuProps = PropsWithChildren<{

View file

@ -1,29 +0,0 @@
import { PropsWithChildren } from 'react'
import type {
DropdownItemProps,
DropdownProps,
DropdownToggleProps,
} from './dropdown-menu-props'
import type { ButtonProps } from './button-props'
type SplitButtonItemProps = Pick<
DropdownItemProps,
'eventKey' | 'leadingIcon'
> & {
label: React.ReactNode
}
export type SplitButtonVariants = Extract<
ButtonProps['variant'],
'primary' | 'secondary' | 'danger' | 'link'
>
export type SplitButtonProps = PropsWithChildren<{
accessibilityLabel?: string
align?: DropdownProps['align']
disabled?: boolean
id: DropdownToggleProps['id']
items: SplitButtonItemProps[]
text: string
variant: SplitButtonVariants
}>

View file

@ -53,7 +53,7 @@ const TextButton: FC<{
return ( return (
<OLButton <OLButton
onClick={handleClick} onClick={handleClick}
size="small" size="sm"
variant="secondary" variant="secondary"
className="copy-button" className="copy-button"
bs3Props={{ bsSize: 'xsmall' }} bs3Props={{ bsSize: 'xsmall' }}

View file

@ -25,7 +25,7 @@ export default function useDropdown(defaultOpen = false) {
// handle dropdown toggle // handle dropdown toggle
const handleToggle = useCallback(value => { const handleToggle = useCallback(value => {
setOpen(value) setOpen(Boolean(value))
}, []) }, [])
// close the dropdown on click outside the dropdown // close the dropdown on click outside the dropdown

View file

@ -17,7 +17,7 @@ export const ButtonStyle = args => {
{...args} {...args}
buttonProps={{ buttonProps={{
variant: 'danger', variant: 'danger',
size: 'large', size: 'lg',
}} }}
/> />
) )

View file

@ -27,11 +27,6 @@ const meta: Meta<typeof Badge> = {
disable: true, disable: true,
}, },
}, },
closeBtnProps: {
table: {
disable: true,
},
},
}, },
} }
export default meta export default meta

View file

@ -1,33 +1,60 @@
import { SplitButton } from '@/features/ui/components/bootstrap-5/split-button' import { Fragment } from 'react'
import type { Meta } from '@storybook/react' import type { Meta } from '@storybook/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
Dropdown,
DropdownHeader,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import Button from '@/features/ui/components/bootstrap-5/button'
import { ButtonGroup } from 'react-bootstrap-5'
type Args = React.ComponentProps<typeof SplitButton> type Args = React.ComponentProps<typeof Dropdown>
export const Dropdown = (args: Args) => { export const Sizes = (args: Args) => {
const { t } = useTranslation() const { t } = useTranslation()
const sizes = {
Large: 'lg',
Regular: undefined,
Small: 'sm',
} as const
const variants = ['primary', 'secondary', 'danger'] as const
return <SplitButton accessibilityLabel={t('expand')} {...args} /> return Object.entries(sizes).map(([label, size]) => (
<Fragment key={`${label}-${size}`}>
<h4>{label}</h4>
<div style={{ display: 'inline-flex', gap: '10px' }}>
{variants.map(variant => (
<Dropdown key={variant} as={ButtonGroup}>
<Button variant={variant} size={size}>
Split Button
</Button>
<DropdownToggle
split
variant={variant}
id={`split-btn-${variant}-${size}`}
size={size}
aria-label={t('expand')}
/>
<DropdownMenu>
<DropdownHeader>Header</DropdownHeader>
<DropdownItem as="button">Action 1</DropdownItem>
<DropdownItem as="button">Action 2</DropdownItem>
<DropdownItem as="button">Action 3</DropdownItem>
</DropdownMenu>
</Dropdown>
))}
</div>
</Fragment>
))
} }
const meta: Meta<typeof SplitButton> = { const meta: Meta<typeof Dropdown> = {
title: 'Shared/Components/Bootstrap 5/SplitButton', title: 'Shared/Components/Bootstrap 5/SplitButton',
component: SplitButton, component: Dropdown,
args: { args: {
align: { sm: 'start' }, align: { sm: 'start' },
id: 'split-button',
items: [
{ eventKey: '1', label: 'Action 1' },
{ eventKey: '2', label: 'Action 2' },
{ eventKey: '3', label: 'Action 3' },
],
text: 'Split Button',
},
argTypes: {
id: {
table: {
disable: true,
},
},
}, },
parameters: { parameters: {
bootstrap5: true, bootstrap5: true,

View file

@ -53,9 +53,6 @@
outline: none; outline: none;
} }
} }
.btn-toggle-logs-label {
padding-left: @line-height-computed / 4;
}
.pdf-toolbar-btn { .pdf-toolbar-btn {
display: inline-block; display: inline-block;
@ -64,7 +61,6 @@
padding: 4px 2px; padding: 4px 2px;
line-height: 1; line-height: 1;
height: 24px; height: 24px;
border-radius: 2px;
border-radius: @border-radius-base; border-radius: @border-radius-base;
&:hover, &:hover,

View file

@ -67,11 +67,11 @@
// Toolbar // Toolbar
@mixin toolbar-sm-height { @mixin toolbar-sm-height {
height: 32px; height: var(--toolbar-small-height);
} }
@mixin toolbar-alt-bg() { @mixin toolbar-alt-bg() {
background-color: var(--bg-dark-secondary); background-color: var(--toolbar-alt-bg-color);
} }
@mixin theme($name) { @mixin theme($name) {
@ -87,3 +87,20 @@
@mixin box-shadow-button-input { @mixin box-shadow-button-input {
box-shadow: 0 0 0 2px var(--blue-30); box-shadow: 0 0 0 2px var(--blue-30);
} }
@mixin animation($animation) {
animation: $animation;
}
@mixin striped($color: rgba(255, 255, 255, 0.15), $angle: 45deg) {
background-image: linear-gradient(
$angle,
$color 25%,
transparent 25%,
transparent 50%,
$color 50%,
$color 75%,
transparent 75%,
transparent
);
}

View file

@ -2,7 +2,6 @@
@import 'button-group'; @import 'button-group';
@import 'dropdown-menu'; @import 'dropdown-menu';
@import 'image'; @import 'image';
@import 'split-button';
@import 'notifications'; @import 'notifications';
@import 'system-messages'; @import 'system-messages';
@import 'tooltip'; @import 'tooltip';

View file

@ -1,10 +1,10 @@
.btn-group { .btn-group {
> .btn { > .btn {
&:first-child { &:first-of-type {
padding-left: var(--spacing-05); padding-left: var(--spacing-05);
} }
&:last-child { &:last-of-type {
padding-right: var(--spacing-05); padding-right: var(--spacing-05);
} }
} }

View file

@ -138,6 +138,7 @@
border-top-left-radius: 0; border-top-left-radius: 0;
padding-right: var(--spacing-05); padding-right: var(--spacing-05);
padding-left: var(--spacing-05); padding-left: var(--spacing-05);
margin-left: 0;
&.btn-primary, &.btn-primary,
&.btn-danger { &.btn-danger {

View file

@ -1,9 +0,0 @@
.split-button {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
border-right: none;
}
.split-button-caret {
vertical-align: middle;
}

View file

@ -12,5 +12,7 @@
@import 'editor/file-tree'; @import 'editor/file-tree';
@import 'editor/figure-modal'; @import 'editor/figure-modal';
@import 'subscription'; @import 'subscription';
@import 'editor/pdf';
@import 'editor/compile-button';
@import 'website-redesign'; @import 'website-redesign';
@import 'group-settings'; @import 'group-settings';

View file

@ -0,0 +1,31 @@
$stripe-width: 20px;
@keyframes pdf-toolbar-stripes {
from {
background-position: 0 0;
}
to {
background-position: $stripe-width 0;
}
}
.btn-striped-animated {
@include striped;
background-size: $stripe-width $stripe-width;
background-origin: content-box;
@include animation(pdf-toolbar-stripes 2s linear infinite);
}
.detach-compile-button {
&[disabled],
&[disabled]:active {
background-color: var(--bs-btn-bg);
color: var(--bs-btn-color);
opacity: 1;
pointer-events: auto;
cursor: not-allowed;
}
}

View file

@ -0,0 +1,357 @@
:root {
--pdf-bg: var(--neutral-10);
--pdf-toolbar-btn-hover-color: rgb(125 125 125 / 20%);
}
@include theme('light') {
--pdf-toolbar-btn-hover-color: var(--neutral-10);
}
.pdf .toolbar.toolbar-pdf {
@include toolbar-sm-height;
@include toolbar-alt-bg;
padding-right: var(--spacing-03);
margin-left: 0;
.btn.disabled,
.btn[disabled] {
pointer-events: auto;
cursor: not-allowed;
opacity: 1;
}
}
.toolbar-pdf-left {
gap: var(--spacing-02);
.dropdown > .btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
&[disabled],
&[disabled]:active {
background-color: var(--bs-btn-bg);
color: var(--bs-btn-color);
opacity: 1;
pointer-events: auto;
cursor: not-allowed;
}
}
}
.toolbar-pdf-orphan,
.toolbar-pdf-left,
.toolbar-pdf-right,
.toolbar-pdf-controls {
display: flex;
align-items: center;
align-self: stretch;
}
.toolbar-pdf-orphan,
.toolbar-pdf-controls {
flex: 1 1 100%;
}
.toolbar-pdf-controls {
margin-right: var(--spacing-02);
justify-content: flex-end;
}
.toolbar-pdf-right {
flex: 1;
justify-content: flex-end;
}
.toolbar-pdf-orphan {
justify-content: center;
color: var(--toolbar-btn-color);
.btn {
margin-left: var(--spacing-03);
}
}
.btn.pdf-toolbar-btn {
display: inline-block;
color: var(--toolbar-btn-color);
background-color: transparent;
padding: 0 var(--spacing-01);
line-height: 1;
height: 24px;
border-radius: var(--border-radius-base);
text-decoration: none;
&:hover,
&:active,
&:focus {
color: var(--toolbar-btn-color);
}
&:hover {
&:not(:disabled) {
background-color: var(--pdf-toolbar-btn-hover-color);
}
}
&:active {
background-color: transparent;
}
.button-content {
align-self: center;
}
.badge {
font-size: 60%;
}
&.log-btn {
border: none;
&.active {
color: var(--white);
background-color: var(--link-color);
box-shadow: none;
opacity: 0.65;
&:hover {
&:not(:disabled) {
background-color: transparent;
color: var(--toolbar-btn-color);
}
}
}
&:focus {
outline: none;
}
}
}
.pdf {
background-color: var(--pdf-bg);
}
.pdf-viewer,
.pdf-logs,
.pdf-errors,
.pdf-uncompiled {
@extend .full-size;
top: var(--toolbar-small-height);
}
.pdf-viewer {
iframe {
width: 100%;
height: 100%;
border: none;
}
.pdfjs-viewer {
@extend .full-size;
background-color: transparent;
overflow: scroll;
/* stylelint-disable-next-line selector-class-pattern */
.canvasWrapper > canvas,
div.pdf-canvas {
background: white;
box-shadow: 0 0 10px rgb(0 0 0 / 50%);
}
div.pdf-canvas.pdfng-empty {
background-color: var(--white);
}
div.pdf-canvas.pdfng-loading {
background-color: var(--white);
}
.page-container {
margin: var(--spacing-05) auto;
padding: 0 var(--spacing-05);
box-sizing: content-box;
user-select: none;
}
.page {
box-sizing: content-box;
margin: var(--spacing-05) auto;
box-shadow: 0 0 8px #bbb;
border: none;
}
.pdfjs-viewer-inner {
position: absolute;
overflow-y: scroll;
width: 100%;
height: 100%;
-webkit-font-smoothing: initial;
-moz-osx-font-smoothing: initial;
/* fix review-panel overflow issue, see: https://github.com/overleaf/internal/issues/6781#issuecomment-1112708638 */
/* stylelint-disable-next-line selector-class-pattern */
.pdfViewer {
min-height: 100%;
}
}
&:focus-within {
outline: none;
}
/* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */
/* stylelint-disable-next-line selector-class-pattern */
.textLayer br::selection {
background: transparent;
}
}
.progress-thin {
position: absolute;
top: -2px;
height: 3px;
left: 0;
right: 0;
.progress-bar {
height: 100%;
background-color: var(--link-color);
}
}
}
.pdfjs-viewer-controls {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
}
.pdfjs-zoom-controls {
display: inline-flex;
border-left: 1px solid rgb(125 125 125 / 30%);
}
.pdfjs-toolbar-buttons {
display: flex;
gap: var(--spacing-04);
margin-left: var(--spacing-04);
margin-right: var(--spacing-04);
}
.pdfjs-toolbar-button {
padding: var(--spacing-01) !important;
display: flex;
align-items: center;
}
.pdfjs-zoom-dropdown-button {
width: 60px;
text-align: right;
font-weight: normal;
}
.pdfjs-zoom-dropdown-mac-shortcut-char {
display: inline-block;
width: 1em;
text-align: center;
}
.pdfjs-custom-zoom-menu-item {
display: block;
pointer-events: initial !important;
&:hover {
background-color: initial !important;
color: initial !important;
cursor: initial !important;
}
}
.pdfjs-page-number-input {
color: var(--toolbar-btn-color);
font-size: var(--font-size-02);
padding-right: var(--spacing-04);
display: flex;
align-items: center;
gap: var(--spacing-02);
input {
color: initial;
border: 1px solid var(--neutral-60);
width: 32px;
height: 24px;
border-radius: var(--border-radius-base);
text-align: center;
}
}
.pdfjs-viewer-controls-small {
display: flex;
align-items: center;
gap: var(--spacing-04);
}
.pdfjs-toolbar-popover-button {
padding: var(--spacing-01) !important;
}
.pdfjs-toolbar-popover {
background-color: var(--editor-toolbar-bg);
border-radius: var(--border-radius-base);
.popover-arrow {
display: none;
}
button {
background-color: transparent;
color: var(--toolbar-btn-color);
}
.popover-body {
display: flex;
align-items: center;
padding: var(--spacing-04) 0;
}
}
// The new viewer UI has overflow on the inner element,
// so disable the overflow on the outer element
.pdf-viewer .pdfjs-viewer.pdfjs-viewer-outer {
overflow: hidden;
}
:fullscreen {
.pdfjs-viewer-inner {
overflow-y: hidden !important;
}
}
.synctex-control {
> .synctex-control-icon {
display: inline-block;
font:
normal normal normal 14px/1 FontAwesome,
sans-serif;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
> .synctex-spin-icon {
margin-top: var(--spacing-01);
}
}
.keyboard-tooltip {
.tooltip-inner {
max-width: none;
}
}

View file

@ -7,6 +7,8 @@
--toolbar-btn-hover-color: var(--white); --toolbar-btn-hover-color: var(--white);
--toolbar-btn-active-color: var(--white); --toolbar-btn-active-color: var(--white);
--toolbar-btn-active-bg-color: var(--green-50); --toolbar-btn-active-bg-color: var(--green-50);
--toolbar-small-height: 32px;
--toolbar-alt-bg-color: var(--neutral-80);
--formatting-btn-color: var(--white); --formatting-btn-color: var(--white);
--formatting-btn-bg: var(--neutral-80); --formatting-btn-bg: var(--neutral-80);
--formatting-btn-border: var(--neutral-70); --formatting-btn-border: var(--neutral-70);
@ -27,6 +29,7 @@
--toolbar-btn-hover-bg-color: var(--neutral-10); --toolbar-btn-hover-bg-color: var(--neutral-10);
--toolbar-btn-hover-color: var(--neutral-70); --toolbar-btn-hover-color: var(--neutral-70);
--toolbar-btn-active-bg-color: var(--green-50); --toolbar-btn-active-bg-color: var(--green-50);
--toolbar-alt-bg-color: var(--white);
--formatting-btn-color: var(--neutral-70); --formatting-btn-color: var(--neutral-70);
--formatting-btn-bg: transparent; --formatting-btn-bg: transparent;
--formatting-btn-border: var(--neutral-20); --formatting-btn-border: var(--neutral-20);
@ -43,7 +46,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
height: var(--toolbar-height); height: var(--toolbar-height);
min-height: var(--toolbar-height);
border-bottom: 1px solid var(--toolbar-border-color); border-bottom: 1px solid var(--toolbar-border-color);
button { button {

View file

@ -44,7 +44,7 @@ describe('start free trial button', function () {
source="cypress-test" source="cypress-test"
buttonProps={{ buttonProps={{
variant: 'danger', variant: 'danger',
size: 'large', size: 'lg',
}} }}
/> />
) )