Merge pull request #20205 from overleaf/ii-bs5-ide-header

[web] BS5 ide page header

GitOrigin-RevId: b7aad221d458d8403d60ff9950129394c74af856
This commit is contained in:
ilkin-overleaf
2024-09-04 10:52:08 +03:00
committed by Copybot
parent 7add11a190
commit 86689a6269
22 changed files with 770 additions and 170 deletions
@@ -1,20 +1,20 @@
import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap'
import MaterialIcon from '@/shared/components/material-icon'
import OLButton from '@/features/ui/components/ol/ol-button'
function BackToEditorButton({ onClick }: { onClick: () => void }) {
const { t } = useTranslation()
return (
<Button
bsSize="sm"
bsStyle={null}
<OLButton
variant="secondary"
size="small"
onClick={onClick}
className="back-to-editor-btn"
>
<MaterialIcon type="arrow_back" className="toolbar-btn-secondary-icon" />
<p className="toolbar-label">{t('back_to_editor')}</p>
</Button>
<span className="toolbar-label">{t('back_to_editor')}</span>
</OLButton>
)
}
@@ -1,13 +1,15 @@
import { useTranslation } from 'react-i18next'
import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon'
import * as eventTracking from '../../../infrastructure/event-tracking'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
function BackToProjectsButton() {
const { t } = useTranslation()
return (
<Tooltip
<OLTooltip
id="back-to-projects"
description={t('back_to_your_projects')}
overlayProps={{ placement: 'right' }}
@@ -21,14 +23,25 @@ function BackToProjectsButton() {
eventTracking.sendMB('navigation-clicked-home')
}}
>
<Icon
type="home"
fw
accessibilityLabel={t('back_to_your_projects')}
<BootstrapVersionSwitcher
bs3={
<Icon
type="home"
fw
accessibilityLabel={t('back_to_your_projects')}
/>
}
bs5={
<MaterialIcon
type="home"
className="align-text-bottom"
accessibilityLabel={t('back_to_your_projects')}
/>
}
/>
</a>
</div>
</Tooltip>
</OLTooltip>
)
}
@@ -2,6 +2,9 @@ import PropTypes from 'prop-types'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
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 OLBadge from '@/features/ui/components/ol/ol-badge'
function ChatToggleButton({ chatIsOpen, unreadMessageCount, onClick }) {
const { t } = useTranslation()
@@ -16,11 +19,25 @@ function ChatToggleButton({ chatIsOpen, unreadMessageCount, onClick }) {
return (
<div className="toolbar-item">
<button className={classes} onClick={onClick}>
<Icon type="comment" fw className={hasUnreadMessages ? 'bounce' : ''} />
{hasUnreadMessages && (
<span className="label label-info">{unreadMessageCount}</span>
)}
<button type="button" className={classes} onClick={onClick}>
<BootstrapVersionSwitcher
bs3={
<Icon
type="comment"
fw
className={classNames({ bounce: hasUnreadMessages })}
/>
}
bs5={
<MaterialIcon
type="chat"
className={classNames('align-middle', {
bounce: hasUnreadMessages,
})}
/>
}
/>
{hasUnreadMessages && <OLBadge bg="info">{unreadMessageCount}</OLBadge>}
<p className="toolbar-label">{t('chat')}</p>
</button>
</div>
@@ -1,14 +1,19 @@
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function HistoryToggleButton({ onClick }) {
const { t } = useTranslation()
return (
<div className="toolbar-item">
<button className="btn btn-full-height" onClick={onClick}>
<Icon type="history" fw />
<button type="button" className="btn btn-full-height" onClick={onClick}>
<BootstrapVersionSwitcher
bs3={<Icon type="history" fw />}
bs5={<MaterialIcon type="history" className="align-middle" />}
/>
<p className="toolbar-label">{t('history')}</p>
</button>
</div>
@@ -1,5 +1,16 @@
import { memo, ReactNode, useCallback } from 'react'
import { Dropdown, MenuItem, MenuItemProps } from 'react-bootstrap'
import { memo, ReactNode, useCallback, forwardRef } from 'react'
import {
Dropdown as BS3Dropdown,
MenuItem,
MenuItemProps,
} from 'react-bootstrap'
import { Spinner } from 'react-bootstrap-5'
import {
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { Trans, useTranslation } from 'react-i18next'
import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon'
@@ -13,6 +24,8 @@ import {
import * as eventTracking from '../../../infrastructure/event-tracking'
import useEventListener from '../../../shared/hooks/use-event-listener'
import { DetachRole } from '@/shared/context/detach-context'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
function IconPlaceholder() {
return <Icon type="" fw />
@@ -42,7 +55,7 @@ function IconPdfOnly() {
return <Icon type="file-pdf-o" fw />
}
function IconCheckmark({
const isActiveDropdownItem = ({
iconFor,
pdfLayout,
view,
@@ -52,23 +65,26 @@ function IconCheckmark({
pdfLayout: IdeLayout
view: IdeView | null
detachRole?: DetachRole
}) {
}) => {
if (detachRole === 'detacher' || view === 'history') {
return <IconPlaceholder />
return false
}
if (
iconFor === 'editorOnly' &&
pdfLayout === 'flat' &&
(view === 'editor' || view === 'file')
) {
return <IconChecked />
return true
} else if (iconFor === 'pdfOnly' && pdfLayout === 'flat' && view === 'pdf') {
return <IconChecked />
return true
} else if (iconFor === 'sideBySide' && pdfLayout === 'sideBySide') {
return <IconChecked />
return true
}
// return empty icon for placeholder
return <IconPlaceholder />
return false
}
function IconCheckmark(props: Parameters<typeof isActiveDropdownItem>[0]) {
return isActiveDropdownItem(props) ? <IconChecked /> : <IconPlaceholder />
}
function LayoutMenuItem({
@@ -94,6 +110,35 @@ function LayoutMenuItem({
)
}
function EnhancedDropdownItem({
active,
...props
}: React.ComponentProps<typeof DropdownItem>) {
return (
<DropdownItem
active={active}
aria-current={active}
trailingIcon={active ? 'check' : null}
{...props}
/>
)
}
const LayoutDropdownToggleButton = forwardRef<
HTMLButtonElement,
{
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
}
>(({ onClick, ...props }, ref) => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
eventTracking.sendMB('navigation-clicked-layout')
onClick(e)
}
return <button {...props} ref={ref} onClick={handleClick} />
})
LayoutDropdownToggleButton.displayName = 'LayoutDropdownToggleButton'
function DetachDisabled() {
const { t } = useTranslation()
@@ -164,97 +209,209 @@ function LayoutDropdownButton() {
{t('layout_processing')}
</div>
)}
<ControlledDropdown
id="layout-dropdown"
onMainButtonClick={() => {
eventTracking.sendMB('navigation-clicked-layout')
}}
className="toolbar-item layout-dropdown"
pullRight
>
<Dropdown.Toggle className="btn-full-height" bsStyle="link">
{processing ? <IconRefresh /> : <IconLayout />}
<span className="toolbar-label">{t('layout')}</span>
</Dropdown.Toggle>
<Dropdown.Menu className="layout-dropdown-list">
<LayoutMenuItem
onSelect={() => handleChangeLayout('sideBySide')}
checkmark={
<IconCheckmark
iconFor="sideBySide"
pdfLayout={pdfLayout}
view={view}
detachRole={detachRole}
<BootstrapVersionSwitcher
bs3={
<ControlledDropdown
id="layout-dropdown"
onMainButtonClick={() => {
eventTracking.sendMB('navigation-clicked-layout')
}}
className="toolbar-item layout-dropdown"
pullRight
>
<BS3Dropdown.Toggle className="btn-full-height" bsStyle="link">
{processing ? <IconRefresh /> : <IconLayout />}
<span className="toolbar-label">{t('layout')}</span>
</BS3Dropdown.Toggle>
<BS3Dropdown.Menu className="layout-dropdown-list">
<LayoutMenuItem
onSelect={() => handleChangeLayout('sideBySide')}
checkmark={
<IconCheckmark
iconFor="sideBySide"
pdfLayout={pdfLayout}
view={view}
detachRole={detachRole}
/>
}
icon={<IconSplit />}
text={t('editor_and_pdf')}
/>
}
icon={<IconSplit />}
text={t('editor_and_pdf')}
/>
<LayoutMenuItem
onSelect={() => handleChangeLayout('flat', 'editor')}
checkmark={
<IconCheckmark
iconFor="editorOnly"
pdfLayout={pdfLayout}
view={view}
detachRole={detachRole}
<LayoutMenuItem
onSelect={() => handleChangeLayout('flat', 'editor')}
checkmark={
<IconCheckmark
iconFor="editorOnly"
pdfLayout={pdfLayout}
view={view}
detachRole={detachRole}
/>
}
icon={<IconEditorOnly />}
text={
<Trans
i18nKey="editor_only_hide_pdf"
components={[
<span key="editor_only_hide_pdf" className="subdued" />,
]}
/>
}
/>
}
icon={<IconEditorOnly />}
text={
<Trans
i18nKey="editor_only_hide_pdf"
components={[
<span key="editor_only_hide_pdf" className="subdued" />,
]}
/>
}
/>
<LayoutMenuItem
onSelect={() => handleChangeLayout('flat', 'pdf')}
checkmark={
<IconCheckmark
iconFor="pdfOnly"
pdfLayout={pdfLayout}
view={view}
detachRole={detachRole}
<LayoutMenuItem
onSelect={() => handleChangeLayout('flat', 'pdf')}
checkmark={
<IconCheckmark
iconFor="pdfOnly"
pdfLayout={pdfLayout}
view={view}
detachRole={detachRole}
/>
}
icon={<IconPdfOnly />}
text={
<Trans
i18nKey="pdf_only_hide_editor"
components={[
<span key="pdf_only_hide_editor" className="subdued" />,
]}
/>
}
/>
}
icon={<IconPdfOnly />}
text={
<Trans
i18nKey="pdf_only_hide_editor"
components={[
<span key="pdf_only_hide_editor" className="subdued" />,
]}
/>
}
/>
{'BroadcastChannel' in window ? (
<LayoutMenuItem
onSelect={() => handleDetach()}
checkmark={
detachRole === 'detacher' ? (
detachIsLinked ? (
<IconChecked />
) : (
<IconRefresh />
)
) : (
<IconPlaceholder />
)
}
icon={<IconDetach />}
text={t('pdf_in_separate_tab')}
/>
) : (
<DetachDisabled />
)}
</Dropdown.Menu>
</ControlledDropdown>
{'BroadcastChannel' in window ? (
<LayoutMenuItem
onSelect={() => handleDetach()}
checkmark={
detachRole === 'detacher' ? (
detachIsLinked ? (
<IconChecked />
) : (
<IconRefresh />
)
) : (
<IconPlaceholder />
)
}
icon={<IconDetach />}
text={t('pdf_in_separate_tab')}
/>
) : (
<DetachDisabled />
)}
</BS3Dropdown.Menu>
</ControlledDropdown>
}
bs5={
<Dropdown className="toolbar-item layout-dropdown" align="end">
<DropdownToggle
id="layout-dropdown-btn"
className="btn-full-height"
as={LayoutDropdownToggleButton}
>
{processing ? (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
) : (
<MaterialIcon type="dock_to_right" className="align-middle" />
)}
<span className="toolbar-label">{t('layout')}</span>
</DropdownToggle>
<DropdownMenu className="layout-dropdown-list">
<EnhancedDropdownItem
onClick={() => handleChangeLayout('sideBySide')}
active={isActiveDropdownItem({
iconFor: 'sideBySide',
pdfLayout,
view,
detachRole,
})}
leadingIcon="dock_to_right"
>
{t('editor_and_pdf')}
</EnhancedDropdownItem>
<EnhancedDropdownItem
onClick={() => handleChangeLayout('flat', 'editor')}
active={isActiveDropdownItem({
iconFor: 'editorOnly',
pdfLayout,
view,
detachRole,
})}
leadingIcon="code"
>
<Trans
i18nKey="editor_only_hide_pdf"
components={[
<span
key="editor_only_hide_pdf"
className="ms-1 subdued"
/>,
]}
/>
</EnhancedDropdownItem>
<EnhancedDropdownItem
onClick={() => handleChangeLayout('flat', 'pdf')}
active={isActiveDropdownItem({
iconFor: 'pdfOnly',
pdfLayout,
view,
detachRole,
})}
leadingIcon="picture_as_pdf"
>
<Trans
i18nKey="pdf_only_hide_editor"
components={[
<span
key="pdf_only_hide_editor"
className="ms-1 subdued"
/>,
]}
/>
</EnhancedDropdownItem>
{'BroadcastChannel' in window ? (
<EnhancedDropdownItem
onClick={() => handleDetach()}
active={detachRole === 'detacher' && detachIsLinked}
trailingIcon={
detachRole === 'detacher' ? (
detachIsLinked ? (
'check'
) : (
<span className="spinner-container">
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
<span className="visually-hidden">
{t('loading')}
</span>
</span>
)
) : null
}
leadingIcon="select_window"
>
{t('pdf_in_separate_tab')}
</EnhancedDropdownItem>
) : (
<DetachDisabled />
)}
</DropdownMenu>
</Dropdown>
}
/>
</>
)
}
@@ -1,14 +1,24 @@
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function MenuButton({ onClick }) {
const { t } = useTranslation()
return (
<div className="toolbar-item">
<button className="btn btn-full-height" onClick={onClick}>
<Icon type="bars" fw className="editor-menu-icon" />
<button type="button" className="btn btn-full-height" onClick={onClick}>
<BootstrapVersionSwitcher
bs3={<Icon type="bars" fw className="editor-menu-icon" />}
bs5={
<MaterialIcon
type="menu"
className="editor-menu-icon align-middle"
/>
}
/>
<p className="toolbar-label">{t('menu')}</p>
</button>
</div>
@@ -1,11 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import { Dropdown, MenuItem } from 'react-bootstrap'
import Tooltip from '../../../shared/components/tooltip'
import { Dropdown as BS3Dropdown, MenuItem } from 'react-bootstrap'
import {
Dropdown,
DropdownHeader,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import Icon from '../../../shared/components/icon'
import { getHueForUserId } from '../../../shared/utils/colors'
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
function OnlineUsersWidget({ onlineUsers, goToUser }) {
const { t } = useTranslation()
@@ -14,31 +23,63 @@ function OnlineUsersWidget({ onlineUsers, goToUser }) {
if (shouldDisplayDropdown) {
return (
<ControlledDropdown id="online-users" className="online-users" pullRight>
<DropDownToggleButton
bsRole="toggle"
onlineUserCount={onlineUsers.length}
/>
<Dropdown.Menu>
<MenuItem header>{t('connected_users')}</MenuItem>
{onlineUsers.map((user, index) => (
<MenuItem
as="button"
key={`${user.user_id}_${index}`}
eventKey={user}
onSelect={goToUser}
>
<UserIcon user={user} onClick={goToUser} showName />
</MenuItem>
))}
</Dropdown.Menu>
</ControlledDropdown>
<BootstrapVersionSwitcher
bs3={
<ControlledDropdown
id="online-users"
className="online-users"
pullRight
>
<DropDownToggleButton
bsRole="toggle"
onlineUserCount={onlineUsers.length}
/>
<BS3Dropdown.Menu>
<MenuItem header>{t('connected_users')}</MenuItem>
{onlineUsers.map((user, index) => (
<MenuItem
as="button"
key={`${user.user_id}_${index}`}
eventKey={user}
onSelect={goToUser}
>
<UserIcon user={user} showName />
</MenuItem>
))}
</BS3Dropdown.Menu>
</ControlledDropdown>
}
bs5={
<Dropdown id="online-users" className="online-users" align="end">
<DropdownToggle
as={DropDownToggleButton}
onlineUserCount={onlineUsers.length}
/>
<DropdownMenu>
<DropdownHeader aria-hidden="true">
{t('connected_users')}
</DropdownHeader>
{onlineUsers.map((user, index) => (
<li role="none" key={`${user.user_id}_${index}`}>
<DropdownItem
as="button"
tabIndex={-1}
onClick={() => goToUser(user)}
>
<UserIcon user={user} showName />
</DropdownItem>
</li>
))}
</DropdownMenu>
</Dropdown>
}
/>
)
} else {
return (
<div className="online-users">
{onlineUsers.map((user, index) => (
<Tooltip
<OLTooltip
key={`${user.user_id}_${index}`}
id="online-user"
description={user.name}
@@ -48,7 +89,7 @@ function OnlineUsersWidget({ onlineUsers, goToUser }) {
{/* OverlayTrigger won't fire unless UserIcon is wrapped in a span */}
<UserIcon user={user} onClick={goToUser} />
</span>
</Tooltip>
</OLTooltip>
))}
</div>
)
@@ -69,7 +110,7 @@ function UserIcon({ user, showName, onClick }) {
const backgroundColor = `hsl(${getHueForUserId(user.user_id)}, 70%, 50%)`
function handleOnClick() {
onClick(user)
onClick?.(user)
}
const [character] = [...user.name]
@@ -91,25 +132,30 @@ UserIcon.propTypes = {
name: PropTypes.string.isRequired,
}),
showName: PropTypes.bool,
onClick: PropTypes.func.isRequired,
onClick: PropTypes.func,
}
const DropDownToggleButton = React.forwardRef((props, ref) => {
const { t } = useTranslation()
return (
<Tooltip
<OLTooltip
id="connected-users"
description={t('connected_users')}
overlayProps={{ placement: 'left' }}
>
<button
className="btn online-user online-user-multi"
type="button"
className="online-user online-user-multi"
onClick={props.onClick} // required by Bootstrap Dropdown to trigger an opening
ref={ref}
>
<strong>{props.onlineUserCount}</strong>
<Icon type="users" fw />
<strong>{props.onlineUserCount}</strong>&nbsp;
<BootstrapVersionSwitcher
bs3={<Icon type="users" fw />}
bs5={<MaterialIcon type="groups" />}
/>
</button>
</Tooltip>
</OLTooltip>
)
})
@@ -1,8 +1,11 @@
import { useEffect, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon'
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
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 ProjectNameEditableLabelProps = {
projectName: string
@@ -70,10 +73,9 @@ function ProjectNameEditableLabel({
</span>
)}
{isRenaming && (
<input
<OLFormControl
ref={inputRef}
type="text"
className="form-control"
onKeyDown={handleKeyDown}
onChange={handleOnChange}
onBlur={handleBlur}
@@ -81,16 +83,19 @@ function ProjectNameEditableLabel({
/>
)}
{canRename && (
<Tooltip
<OLTooltip
id="online-user"
description={t('rename')}
overlayProps={{ placement: 'bottom', trigger: ['hover', 'focus'] }}
>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus */}
<a className="rename" role="button" onClick={startRenaming}>
<Icon type="pencil" fw />
<BootstrapVersionSwitcher
bs3={<Icon type="pencil" fw />}
bs5={<MaterialIcon type="edit" className="align-text-bottom" />}
/>
</a>
</Tooltip>
</OLTooltip>
)}
</div>
)
@@ -1,14 +1,19 @@
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function ShareProjectButton({ onClick }) {
const { t } = useTranslation()
return (
<div className="toolbar-item">
<button className="btn btn-full-height" onClick={onClick}>
<Icon type="group" fw />
<button type="button" className="btn btn-full-height" onClick={onClick}>
<BootstrapVersionSwitcher
bs3={<Icon type="group" fw />}
bs5={<MaterialIcon type="groups" className="align-middle" />}
/>
<p className="toolbar-label">{t('share')}</p>
</button>
</div>
@@ -16,6 +16,7 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.mac
import BackToEditorButton from './back-to-editor-button'
import getMeta from '@/utils/meta'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
const [publishModalModules] = importOverleafModules('publishModal')
const PublishButton = publishModalModules?.import.default
@@ -55,7 +56,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
return (
<header
className="toolbar toolbar-header toolbar-with-labels"
className="toolbar toolbar-header"
role="navigation"
aria-label={t('project_layout_sharing_submission')}
>
@@ -72,7 +73,16 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
}
)}
</div>
{getMeta('ol-showUpgradePrompt') && <UpgradePrompt />}
{getMeta('ol-showUpgradePrompt') && (
<BootstrapVersionSwitcher
bs3={<UpgradePrompt />}
bs5={
<div className="d-flex align-items-center">
<UpgradePrompt />
</div>
}
/>
)}
<ProjectNameEditableLabel
className="toolbar-center"
projectName={projectName}
@@ -84,7 +94,14 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
<OnlineUsersWidget onlineUsers={onlineUsers} goToUser={goToUser} />
{historyIsOpen ? (
<BackToEditorButton onClick={toggleHistoryOpen} />
<BootstrapVersionSwitcher
bs3={<BackToEditorButton onClick={toggleHistoryOpen} />}
bs5={
<div className="d-flex align-items-center">
<BackToEditorButton onClick={toggleHistoryOpen} />
</div>
}
/>
) : (
<>
{trackChangesVisible && (
@@ -15,7 +15,12 @@ function TrackChangesToggleButton({
return (
<div className="toolbar-item">
<button disabled={disabled} className={classes} onMouseDown={onMouseDown}>
<button
type="button"
disabled={disabled}
className={classes}
onMouseDown={onMouseDown}
>
<i className="review-icon" />
<p className="toolbar-label">{t('review')}</p>
</button>
@@ -1,5 +1,8 @@
import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking'
import OLButton from '@/features/ui/components/ol/ol-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
function UpgradePrompt() {
const { t } = useTranslation()
@@ -10,14 +13,19 @@ function UpgradePrompt() {
}
return (
<a
className="toolbar-header-upgrade-prompt btn btn-primary btn-xs"
<OLButton
variant="primary"
size="small"
className={classnames(
'toolbar-header-upgrade-prompt',
bsVersion({ bs3: 'btn-xs' })
)}
href="/user/subscription/plans?itm_referrer=editor-header-upgrade-prompt"
target="_blank"
onClick={handleClick}
>
{t('upgrade')}
</a>
</OLButton>
)
}
@@ -99,7 +99,7 @@ const ForwardReferredDropdownItem = fixedForwardRef(DropdownItem, {
export { ForwardReferredDropdownItem as DropdownItem }
export function DropdownToggle({ ...props }: DropdownToggleProps) {
export function DropdownToggle(props: DropdownToggleProps) {
return <BS5DropdownToggle {...props} />
}
@@ -35,6 +35,7 @@ const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
onChange: rest.onChange as BS3FormControlProps['onChange'],
onKeyDown: rest.onKeyDown as BS3FormControlProps['onKeyDown'],
onFocus: rest.onFocus as BS3FormControlProps['onFocus'],
onBlur: rest.onBlur as BS3FormControlProps['onBlur'],
onInvalid: rest.onInvalid as BS3FormControlProps['onInvalid'],
inputRef: (inputElement: HTMLInputElement) => {
if (typeof ref === 'function') {
@@ -60,8 +60,6 @@ export const Active = (args: Args) => {
}
export const MultipleSelection = (args: Args) => {
console.log('DropdownItem.EmptyLeadingIcon', DropdownItem.EmptyLeadingIcon)
return (
<DropdownMenu show>
<DropdownHeader>Header</DropdownHeader>
@@ -14,6 +14,8 @@
}
.online-user-multi {
.reset-button;
color: @toolbar-btn-color;
width: auto;
min-width: 24px;
@@ -2,3 +2,40 @@
visibility: hidden;
height: 0 !important; // Prevent layout shift
}
@keyframes bounce {
0%,
10%,
26%,
40% {
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
transform: translate3d(0, 0, 0);
}
20%,
21% {
transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -10px, 0);
}
35% {
transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -5px, 0);
}
45% {
transform: translate3d(0, -2px, 0);
}
50% {
transform: translate3d(0, 0, 0);
}
}
.bounce {
animation-duration: 2s;
animation-fill-mode: both;
animation-iteration-count: infinite;
animation-name: bounce;
transform-origin: center bottom;
}
@@ -17,6 +17,10 @@
&.sm {
min-width: 160px;
}
.subdued {
color: var(--content-disabled);
}
}
.dropdown-item {
@@ -2,7 +2,9 @@
@import 'project-list';
@import 'sidebar-v2-dash-pane';
@import 'editor/ide';
@import 'editor/toolbar';
@import 'editor/online-users';
@import 'editor/review-panel';
@import 'editor/loading-screen';
@import 'editor/outline';
@import 'editor/file-tree';
@import 'editor/toolbar';
@@ -0,0 +1,33 @@
:root {
--toolbar-btn-color: var(--white);
}
.online-users {
display: flex;
align-items: center;
.online-user {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
margin-right: var(--spacing-04);
text-align: center;
color: white;
text-transform: uppercase;
border-radius: var(--border-radius-base);
cursor: pointer;
}
.online-user-multi {
@include reset-button;
color: var(--toolbar-btn-color);
width: auto;
min-width: 24px;
padding-left: var(--spacing-04);
padding-right: var(--spacing-03);
display: flex;
align-items: center;
}
}
@@ -0,0 +1,16 @@
:root {
--review-icon: url('../../../../../public/img/ol-icons/review-icon-dark-theme.svg');
}
.review-icon {
display: inline-block;
background-image: var(--review-icon);
background-repeat: no-repeat;
background-position: top;
background-size: 30px;
width: 30px;
&::before {
content: '\00a0'; // Non-breakable space. A non-breakable character here makes this icon work like font-awesome.
}
}
@@ -1,8 +1,227 @@
$toolbar-height: 40px;
:root {
--toolbar-border-color: var(--neutral-80);
--toolbar-header-bg-color: var(--neutral-90);
--toolbar-header-btn-border-color: var(--neutral-80);
--toolbar-btn-color: var(--white);
--toolbar-btn-hover-bg-color: var(--neutral-80);
--project-name-color: var(--neutral-40);
--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;
}
.toolbar {
--toolbar-height: 40px;
display: flex;
align-items: center;
height: $toolbar-height;
border-bottom: 1px solid var(--border-divider-dark);
align-items: stretch;
height: var(--toolbar-height);
min-height: var(--toolbar-height);
border-bottom: 1px solid var(--toolbar-border-color);
button {
position: relative;
.badge {
position: absolute;
top: 0;
right: 0;
pointer-events: none; // Labels were capturing button/anchor clicks.
.badge-content {
padding: 0;
}
}
}
.toolbar-right,
.toolbar-left {
button {
background: transparent;
box-shadow: none;
}
}
.toolbar-right .back-to-editor-btn {
margin-right: var(--spacing-09);
}
a.btn-full-height,
button.btn-full-height {
border: none;
border-radius: 0;
border-right: 1px solid var(--toolbar-header-btn-border-color) !important;
color: var(--toolbar-btn-color);
padding: var(--spacing-02) var(--spacing-05) var(--spacing-03);
font-size: var(--font-size-05);
max-height: 39px;
&:hover {
text-shadow: none;
background-color: var(--toolbar-btn-hover-bg-color);
color: var(--toolbar-btn-color);
}
&.active,
&:active {
color: var(--white);
background-color: $bg-accent-01;
box-shadow: none;
}
.badge {
top: var(--spacing-01);
right: var(--spacing-02);
}
&.header-cobranding-logo-container {
height: calc(var(--toolbar-height) - 1px);
padding: var(--spacing-04) var(--spacing-05);
}
.spinner-border {
vertical-align: middle;
font-size: var(--font-size-02);
}
&.dropdown-toggle {
line-height: 1;
&::after {
vertical-align: middle;
font-size: var(--font-size-02);
}
}
}
.btn-full-height-no-border {
border-right: 0;
border-left: 0;
}
.toolbar-left {
display: flex;
align-items: stretch;
float: left;
text-align: center;
}
.toolbar-right {
display: flex;
align-items: stretch;
flex-grow: 1;
justify-content: flex-end;
.btn-full-height {
border-right: 0;
border-left: 1px solid var(--toolbar-header-btn-border-color);
}
}
.toolbar-center {
text-align: center;
text-overflow: ellipsis;
overflow: hidden;
// At small screen sizes, center relative to the left menu and right buttons
width: 100%;
display: flex;
justify-content: center;
}
&.toolbar-header {
background-color: var(--toolbar-header-bg-color);
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1;
}
.toolbar-item {
display: flex;
}
}
.header-cobranding-logo {
display: block;
width: auto;
max-height: 100%;
}
.toolbar-label {
display: none;
margin: 0 var(--spacing-02);
font-size: var(--font-size-02);
font-weight: 600;
vertical-align: middle;
text-align: left;
@include media-breakpoint-up(lg) {
display: inline-block;
}
&.toolbar-label-multiline {
line-height: 1.1;
}
}
.toolbar-header-upgrade-prompt {
margin-left: var(--spacing-05);
@include media-breakpoint-down(lg) {
display: none !important;
}
}
.editor-menu-icon {
&.material-symbols {
width: 1em;
text-indent: -9999px;
background: var(--editor-header-logo-background);
}
}
.project-name {
.name {
display: inline-block;
@include text-truncate;
padding: var(--spacing-03);
vertical-align: top;
color: var(--project-name-color);
font-weight: 700;
}
input {
height: 30px;
margin-top: var(--spacing-02);
padding: var(--spacing-03);
max-width: 500px;
text-align: center;
font-weight: 700;
}
a.rename {
visibility: hidden;
display: inline-block;
color: var(--project-rename-link-color);
padding: var(--spacing-03);
border-radius: var(--border-radius-base);
cursor: pointer;
&:hover {
text-shadow: 0 1px 0 rgb(0 0 0 / 25%);
color: var(--project-rename-link-color-hover);
text-decoration: none;
}
}
&:hover {
a.rename {
visibility: visible;
}
}
}