Merge pull request #21240 from overleaf/ii-bs5-review-panel-old

[web] BS5 review panel old

GitOrigin-RevId: da018b8f2946afb21ab63da0003453e20781f04c
This commit is contained in:
ilkin-overleaf 2024-10-23 16:21:00 +03:00 committed by Copybot
parent 92a23b7e9d
commit 5c3d9117c5
25 changed files with 1594 additions and 430 deletions

View file

@ -16,6 +16,8 @@ import useScopeEventListener from '@/shared/hooks/use-scope-event-listener'
import { memo, useCallback } from 'react'
import { useLayoutContext } from '@/shared/context/layout-context'
import classnames from 'classnames'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
function EditorWidgets() {
const { t } = useTranslation()
@ -71,10 +73,20 @@ function EditorWidgets() {
{nChanges > 1 && permissions.write && (
<>
<BulkActions.Button onClick={handleShowBulkAcceptDialog}>
<Icon type="check" /> {t('accept_all')} ({nChanges})
<BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" />}
/>
&nbsp;
{t('accept_all')} ({nChanges})
</BulkActions.Button>
<BulkActions.Button onClick={handleShowBulkRejectDialog}>
<Icon type="times" /> {t('reject_all')} ({nChanges})
<BootstrapVersionSwitcher
bs3={<Icon type="times" />}
bs5={<MaterialIcon type="close" />}
/>
&nbsp;
{t('reject_all')} ({nChanges})
</BulkActions.Button>
</>
)}
@ -83,7 +95,12 @@ function EditorWidgets() {
!isRestrictedTokenMember &&
currentDocEntries?.['add-comment'] && (
<AddCommentButton onClick={handleAddNewCommentClick}>
<Icon type="comment" /> {t('add_comment')}
<BootstrapVersionSwitcher
bs3={<Icon type="comment" />}
bs5={<MaterialIcon type="mode_comment" />}
/>
&nbsp;
{t('add_comment')}
</AddCommentButton>
)}
</div>

View file

@ -11,6 +11,9 @@ import {
useReviewPanelValueContext,
} from '../../../context/review-panel/review-panel-context'
import classnames from 'classnames'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import LoadingSpinner from '@/shared/components/loading-spinner'
function AddCommentEntry() {
const { t } = useTranslation()
@ -116,9 +119,7 @@ function AddCommentEntry() {
<>
<div className="rp-new-comment">
{isSubmitting ? (
<div className="rp-loading">
<Icon type="spinner" spin />
</div>
<LoadingSpinner className="d-flex justify-content-center" />
) : (
<AutoExpandingTextArea
className="rp-comment-input"
@ -137,19 +138,34 @@ function AddCommentEntry() {
className="rp-entry-button-cancel"
onClick={handleCancelNewComment}
>
<Icon type="times" /> {t('cancel')}
<BootstrapVersionSwitcher
bs3={<Icon type="times" />}
bs5={<MaterialIcon type="close" />}
/>
&nbsp;
{t('cancel')}
</EntryActions.Button>
<EntryActions.Button
onClick={handleSubmitNewComment}
disabled={isSubmitting || !content.length}
>
<Icon type="comment" /> {t('comment')}
<BootstrapVersionSwitcher
bs3={<Icon type="comment" />}
bs5={<MaterialIcon type="mode_comment" />}
/>
&nbsp;
{t('comment')}
</EntryActions.Button>
</EntryActions>
</>
) : (
<AddCommentButton onClick={handleStartNewComment}>
<Icon type="comment" /> {t('add_comment')}
<BootstrapVersionSwitcher
bs3={<Icon type="comment" />}
bs5={<MaterialIcon type="mode_comment" />}
/>
&nbsp;
{t('add_comment')}
</AddCommentButton>
)}
</div>

View file

@ -12,6 +12,8 @@ import { BaseChangeEntryProps } from '../types/base-change-entry-props'
import useIndicatorHover from '../hooks/use-indicator-hover'
import EntryIndicator from './entry-indicator'
import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
interface AggregateChangeEntryProps extends BaseChangeEntryProps {
replacedContent: string
@ -80,7 +82,10 @@ function AggregateChangeEntry({
onMouseEnter={handleIndicatorMouseEnter}
onClick={handleIndicatorClick}
>
<Icon type="pencil" />
<BootstrapVersionSwitcher
bs3={<Icon type="pencil" />}
bs5={<MaterialIcon type="edit" />}
/>
</EntryIndicator>
<div
className={classnames('rp-entry', 'rp-entry-aggregate', {
@ -89,7 +94,10 @@ function AggregateChangeEntry({
>
<div className="rp-entry-body">
<div className="rp-entry-action-icon">
<Icon type="pencil" />
<BootstrapVersionSwitcher
bs3={<Icon type="pencil" />}
bs5={<MaterialIcon type="edit" />}
/>
</div>
<div className="rp-entry-details">
<div className="rp-entry-description">
@ -97,7 +105,7 @@ function AggregateChangeEntry({
<del className="rp-content-highlight">{deletionContent}</del>
{deletionNeedsCollapsing && (
<button
className="rp-collapse-toggle btn-inline-link"
className="rp-collapse-toggle"
onClick={handleDeletionToggleCollapse}
>
{isDeletionCollapsed
@ -109,7 +117,7 @@ function AggregateChangeEntry({
<ins className="rp-content-highlight">{insertionContent}</ins>
{insertionNeedsCollapsing && (
<button
className="rp-collapse-toggle btn-inline-link"
className="rp-collapse-toggle"
onClick={handleInsertionToggleCollapse}
>
{isInsertionCollapsed
@ -139,10 +147,18 @@ function AggregateChangeEntry({
{permissions.write && (
<EntryActions>
<EntryActions.Button onClick={() => rejectChanges(entryIds)}>
<Icon type="times" /> {t('reject')}
<BootstrapVersionSwitcher
bs3={<Icon type="times" />}
bs5={<MaterialIcon type="close" />}
/>
&nbsp;{t('reject')}
</EntryActions.Button>
<EntryActions.Button onClick={() => acceptChanges(entryIds)}>
<Icon type="check" /> {t('accept')}
<BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" />}
/>
&nbsp;{t('accept')}
</EntryActions.Button>
</EntryActions>
)}

View file

@ -5,6 +5,8 @@ import Icon from '../../../../../../shared/components/icon'
import BulkActions from './bulk-actions'
import Modal, { useBulkActionsModal } from './modal'
import { ReviewPanelBulkActionsEntry } from '../../../../../../../../types/review-panel/entry'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type BulkActionsEntryProps = {
entryId: ReviewPanelBulkActionsEntry['type']
@ -30,10 +32,18 @@ function BulkActionsEntry({ entryId, nChanges }: BulkActionsEntryProps) {
<EntryCallout className="rp-entry-callout-bulk-actions" />
<BulkActions className="rp-entry">
<BulkActions.Button onClick={handleShowBulkRejectDialog}>
<Icon type="times" /> {t('reject_all')} ({nChanges})
<BootstrapVersionSwitcher
bs3={<Icon type="times" />}
bs5={<MaterialIcon type="close" />}
/>
&nbsp;{t('reject_all')} ({nChanges})
</BulkActions.Button>
<BulkActions.Button onClick={handleShowBulkAcceptDialog}>
<Icon type="check" /> {t('accept_all')} ({nChanges})
<BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" />}
/>
&nbsp;{t('accept_all')} ({nChanges})
</BulkActions.Button>
</BulkActions>
</>

View file

@ -1,8 +1,13 @@
import { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Modal as BootstrapModal } from 'react-bootstrap'
import AccessibleModal from '../../../../../../shared/components/accessible-modal'
import { useReviewPanelUpdaterFnsContext } from '../../../../context/review-panel/review-panel-context'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLButton from '@/features/ui/components/ol/ol-button'
type BulkActionsModalProps = {
show: boolean
@ -22,30 +27,28 @@ function Modal({
const { t } = useTranslation()
return (
<AccessibleModal show={show} onHide={() => setShow(false)}>
<BootstrapModal.Header closeButton>
<h3>{isAccept ? t('accept_all') : t('reject_all')}</h3>
</BootstrapModal.Header>
<BootstrapModal.Body>
<OLModal show={show} onHide={() => setShow(false)}>
<OLModalHeader closeButton>
<OLModalTitle>
{isAccept ? t('accept_all') : t('reject_all')}
</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<p>
{isAccept
? t('bulk_accept_confirm', { nChanges })
: t('bulk_reject_confirm', { nChanges })}
</p>
</BootstrapModal.Body>
<BootstrapModal.Footer>
<Button
bsStyle={null}
className="btn-secondary"
onClick={() => setShow(false)}
>
</OLModalBody>
<OLModalFooter>
<OLButton variant="secondary" onClick={() => setShow(false)}>
{t('cancel')}
</Button>
<Button bsStyle={null} className="btn-primary" onClick={onConfirm}>
</OLButton>
<OLButton variant="primary" onClick={onConfirm}>
{t('ok')}
</Button>
</BootstrapModal.Footer>
</AccessibleModal>
</OLButton>
</OLModalFooter>
</OLModal>
)
}

View file

@ -13,6 +13,8 @@ import comparePropsWithShallowArrayCompare from '../utils/compare-props-with-sha
import useIndicatorHover from '../hooks/use-indicator-hover'
import EntryIndicator from './entry-indicator'
import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
interface ChangeEntryProps extends BaseChangeEntryProps {
type: ReviewPanelChangeEntry['type']
@ -71,7 +73,14 @@ function ChangeEntry({
onMouseEnter={handleIndicatorMouseEnter}
onClick={handleIndicatorClick}
>
{isInsert ? <Icon type="pencil" /> : <i className="rp-icon-delete" />}
{isInsert ? (
<BootstrapVersionSwitcher
bs3={<Icon type="pencil" />}
bs5={<MaterialIcon type="edit" />}
/>
) : (
<i className="rp-icon-delete" />
)}
</EntryIndicator>
<div
className={classnames('rp-entry', `rp-entry-${type}`, {
@ -81,7 +90,10 @@ function ChangeEntry({
<div className="rp-entry-body">
<div className="rp-entry-action-icon">
{isInsert ? (
<Icon type="pencil" />
<BootstrapVersionSwitcher
bs3={<Icon type="pencil" />}
bs5={<MaterialIcon type="edit" />}
/>
) : (
<i className="rp-icon-delete" />
)}
@ -106,7 +118,7 @@ function ChangeEntry({
)}
{needsCollapsing && (
<button
className="rp-collapse-toggle btn-inline-link"
className="rp-collapse-toggle"
onClick={handleToggleCollapse}
>
{isCollapsed
@ -137,10 +149,18 @@ function ChangeEntry({
{permissions.write && (
<EntryActions>
<EntryActions.Button onClick={() => rejectChanges(entryIds)}>
<Icon type="times" /> {t('reject')}
<BootstrapVersionSwitcher
bs3={<Icon type="times" />}
bs5={<MaterialIcon type="close" />}
/>
&nbsp;{t('reject')}
</EntryActions.Button>
<EntryActions.Button onClick={() => acceptChanges(entryIds)}>
<Icon type="check" /> {t('accept')}
<BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" />}
/>
&nbsp;{t('accept')}
</EntryActions.Button>
</EntryActions>
)}

View file

@ -16,6 +16,9 @@ import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel
import useIndicatorHover from '../hooks/use-indicator-hover'
import EntryIndicator from './entry-indicator'
import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import LoadingSpinner from '@/shared/components/loading-spinner'
type CommentEntryProps = {
docId: DocId
@ -120,7 +123,10 @@ function CommentEntry({
onMouseEnter={handleIndicatorMouseEnter}
onClick={handleIndicatorClick}
>
<Icon type="comment" />
<BootstrapVersionSwitcher
bs3={<Icon type="comment" />}
bs5={<MaterialIcon type="mode_comment" />}
/>
</EntryIndicator>
<div
className={classnames('rp-entry', 'rp-entry-comment', {
@ -130,7 +136,7 @@ function CommentEntry({
ref={entryDivRef}
>
{!submitting && (!thread || thread.messages.length === 0) && (
<div className="rp-loading">{t('no_comments')}</div>
<div className="text-center p-1">{t('no_comments')}</div>
)}
<div className="rp-comment-loaded">
{thread.messages.map(comment => (
@ -143,9 +149,7 @@ function CommentEntry({
))}
</div>
{submitting && (
<div className="rp-loading">
<Icon type="spinner" spin />
</div>
<LoadingSpinner className="d-flex justify-content-center" />
)}
{permissions.comment && (
<div className="rp-comment-reply">
@ -163,7 +167,11 @@ function CommentEntry({
<EntryActions>
{permissions.comment && permissions.write && (
<EntryActions.Button onClick={handleAnimateAndCallOnResolve}>
<Icon type="inbox" /> {t('resolve')}
<BootstrapVersionSwitcher
bs3={<Icon type="inbox" />}
bs5={<MaterialIcon type="inbox" />}
/>
&nbsp;{t('resolve')}
</EntryActions.Button>
)}
{permissions.comment && (
@ -171,7 +179,11 @@ function CommentEntry({
onClick={handleOnReply}
disabled={!replyContent.length}
>
<Icon type="reply" /> {t('reply')}
<BootstrapVersionSwitcher
bs3={<Icon type="reply" />}
bs5={<MaterialIcon type="reply" />}
/>
&nbsp;{t('reply')}
</EntryActions.Button>
)}
</EntryActions>

View file

@ -61,7 +61,7 @@ function ResolvedCommentEntry({
<>
&nbsp;
<button
className="rp-collapse-toggle btn-inline-link"
className="rp-collapse-toggle"
onClick={() => setIsCollapsed(value => !value)}
>
{isCollapsed ? `… (${t('show_all')})` : ` (${t('show_less')})`}

View file

@ -8,6 +8,8 @@ import {
import { isCurrentFileView, isOverviewView } from '../../utils/sub-view'
import { useCallback } from 'react'
import { useResizeObserver } from '../../../../shared/hooks/use-resize-observer'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function Nav() {
const { t } = useTranslation()
@ -40,7 +42,10 @@ function Nav() {
})}
onClick={() => handleSetSubview('cur_file')}
>
<Icon type="file-text-o" />
<BootstrapVersionSwitcher
bs3={<Icon type="file-text-o" />}
bs5={<MaterialIcon type="description" className="align-middle" />}
/>
<span className="rp-nav-label">{t('current_file')}</span>
</button>
<button
@ -55,7 +60,10 @@ function Nav() {
})}
onClick={() => handleSetSubview('overview')}
>
<Icon type="list" />
<BootstrapVersionSwitcher
bs3={<Icon type="list" />}
bs5={<MaterialIcon type="list" className="align-middle" />}
/>
<span className="rp-nav-label">{t('overview')}</span>
</button>
</div>

View file

@ -2,11 +2,11 @@ import Container from './container'
import Toggler from './toggler'
import Toolbar from './toolbar/toolbar'
import Nav from './nav'
import Icon from '../../../../shared/components/icon'
import OverviewFile from './overview-file'
import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { memo } from 'react'
import LoadingSpinner from '@/shared/components/loading-spinner'
function OverviewContainer() {
const { isOverviewLoading } = useReviewPanelValueContext()
@ -24,9 +24,7 @@ function OverviewContainer() {
aria-labelledby="review-panel-tab-overview"
>
{isOverviewLoading ? (
<div className="rp-loading">
<Icon type="spinner" spin />
</div>
<LoadingSpinner className="d-flex justify-content-center my-2" />
) : (
docs?.map(doc => (
<OverviewFile

View file

@ -8,6 +8,8 @@ import classnames from 'classnames'
import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel'
import { MainDocument } from '../../../../../../types/project-settings'
import OverviewFileEntries from '@/features/source-editor/components/review-panel/entries/overview-file-entries'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type OverviewFileProps = {
docId: MainDocument['doc']['id']
@ -45,7 +47,10 @@ function OverviewFile({ docId, docPath }: OverviewFileProps) {
'rp-overview-file-header-collapse-on': docCollapsed,
})}
>
<Icon type="angle-down" />
<BootstrapVersionSwitcher
bs3={<Icon type="angle-down" />}
bs5={<MaterialIcon type="expand_more" />}
/>
</span>
{docPath}
{docCollapsed && (

View file

@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react'
import { useEffect, useLayoutEffect, useRef, useCallback } from 'react'
import { useLayoutContext } from '../../../../shared/context/layout-context'
import {
ReviewPanelEntry,
@ -6,7 +6,6 @@ import {
} from '../../../../../../types/review-panel/entry'
import { debugConsole } from '../../../../utils/debugging'
import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
import useEventListener from '../../../../shared/hooks/use-event-listener'
import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel'
import { dispatchReviewPanelLayout } from '../../extensions/changes/change-manager'
import { isEqual } from 'lodash'
@ -214,255 +213,269 @@ function PositionedEntries({
previousLayoutInfoRef.current = initialLayoutInfo
}
const layout = (animate = true) => {
const container = containerRef.current
if (!container) {
return
}
const padding = reviewPanelOpen ? 8 : 4
const toolbarPaddedHeight = reviewPanelOpen ? toolbarHeight + 6 : 0
const navPaddedHeight = reviewPanelOpen ? navHeight + 4 : 0
// Create a list of entry views, typing together DOM elements and model.
// No measuring or style change is done at this point.
const entryViews: EntryView[] = []
// TODO: Look into tying the entry to the DOM element without going via a DOM data attribute
for (const wrapper of container.querySelectorAll<HTMLElement>(
'.rp-entry-wrapper'
)) {
const entryId = wrapper.dataset.entryId as
| EntryView['entryId']
| undefined
if (!entryId) {
throw new Error('Could not find an entry ID')
const layout = useCallback(
(animate = true) => {
const container = containerRef.current
if (!container) {
return
}
const entry = entries.find(value => value[0] === entryId)?.[1]
if (!entry) {
throw new Error(`Could not find an entry for ID ${entryId}`)
const padding = reviewPanelOpen ? 8 : 4
const toolbarPaddedHeight = reviewPanelOpen ? toolbarHeight + 6 : 0
const navPaddedHeight = reviewPanelOpen ? navHeight + 4 : 0
// Create a list of entry views, typing together DOM elements and model.
// No measuring or style change is done at this point.
const entryViews: EntryView[] = []
// TODO: Look into tying the entry to the DOM element without going via a DOM data attribute
for (const wrapper of container.querySelectorAll<HTMLElement>(
'.rp-entry-wrapper'
)) {
const entryId = wrapper.dataset.entryId as
| EntryView['entryId']
| undefined
if (!entryId) {
throw new Error('Could not find an entry ID')
}
const entry = entries.find(value => value[0] === entryId)?.[1]
if (!entry) {
throw new Error(`Could not find an entry for ID ${entryId}`)
}
const indicator = wrapper.querySelector<HTMLElement>(
'.rp-entry-indicator'
)
const box = wrapper.querySelector<HTMLElement>('.rp-entry')
const callout = wrapper.querySelector<HTMLElement>('.rp-entry-callout')
const layoutElement = reviewPanelOpen ? box : indicator
if (box && callout && layoutElement) {
const previousPositions =
previousLayoutInfoRef.current?.positions.find(
pos => pos.entryId === entryId
)?.positions
const hasScreenPos = Boolean(entry.screenPos)
entryViews.push({
entryId,
wrapper,
indicator,
box,
callout,
layout: layoutElement,
hasScreenPos,
height: 0,
entry,
previousPositions,
})
} else {
debugConsole.log(
'Entry wrapper is missing indicator, box or callout, so ignoring',
wrapper
)
}
}
const indicator = wrapper.querySelector<HTMLElement>(
'.rp-entry-indicator'
if (entryViews.length === 0) {
resetLayout()
return
}
entryViews.sort((a, b) => a.entry.offset - b.entry.offset)
// Do the DOM interaction in three phases:
//
// - Apply the `display` property to all elements whose visibility has
// changed. This needs to happen first in order to measure heights.
// - Measure the height of each entry
// - Move each entry without animation to their original position
// relative to the editor content
// - Re-enable animation and position each entry
//
// The idea is to batch DOM reads and writes to avoid layout thrashing. In
// this case, the best we can do is a write phase, a read phase then a
// final write phase.
// See https://web.dev/avoid-large-complex-layouts-and-layout-thrashing/
// First, update display for each entry that needs it
hideOrShowEntries(entryViews)
// Next, measure the height of each entry
for (const entryView of entryViews) {
if (entryView.hasScreenPos) {
entryView.height = entryView.layout.offsetHeight
}
}
// Calculate positions for all positioned entries, starting by calculating
// which entry to put in its desired position and anchor everything else
// around. If there is an explicitly focused entry, use that.
let focusedEntryIndex = entryViews.findIndex(view => view.entry.focused)
if (focusedEntryIndex === -1) {
// There is no explicitly focused entry, so use the focused entry from the
// previous layout. This will be the first entry in the list if there was
// no previous layout.
focusedEntryIndex = Math.min(
previousLayoutInfoRef.current.focusedEntryIndex,
entryViews.length - 1
)
// If the entry from the previous layout has no screen position, fall back
// to the first entry in the list that does.
if (!entryViews[focusedEntryIndex].hasScreenPos) {
focusedEntryIndex = entryViews.findIndex(view => view.hasScreenPos)
}
}
// If there is no entry with a screen position, bail out
if (focusedEntryIndex === -1) {
return
}
const focusedEntryView = entryViews[focusedEntryIndex]
// If the focused entry has no screenPos, we can't position other
// entryViews relative to it, so we position all other entryViews as
// though the focused entry is at the top and the rest follow it
const entryViewsAfter = focusedEntryView.hasScreenPos
? entryViews.slice(focusedEntryIndex + 1)
: [...entryViews]
const entryViewsBefore = focusedEntryView.hasScreenPos
? entryViews.slice(0, focusedEntryIndex).reverse() // Work through backwards, starting with the one just above
: []
debugConsole.log('focusedEntryIndex', focusedEntryIndex)
let lastEntryBottom = 0
let firstEntryTop = 0
// Put the focused entry as close as possible to where it wants to be
if (focusedEntryView.hasScreenPos) {
const focusedEntryScreenPos = focusedEntryView.entry.screenPos
const entryTop = Math.max(focusedEntryScreenPos.y, toolbarPaddedHeight)
updateEntryPositions(focusedEntryView, entryTop, lineHeight)
lastEntryBottom = entryTop + focusedEntryView.height
firstEntryTop = entryTop
}
// Calculate positions for entries that are below the focused entry
calculateEntryViewPositions(
entryViewsAfter,
lineHeight,
(originalTop: number, height: number) => {
const top = Math.max(originalTop, lastEntryBottom + padding)
lastEntryBottom = top + height
return top
}
)
const box = wrapper.querySelector<HTMLElement>('.rp-entry')
const callout = wrapper.querySelector<HTMLElement>('.rp-entry-callout')
const layoutElement = reviewPanelOpen ? box : indicator
if (box && callout && layoutElement) {
const previousPositions = previousLayoutInfoRef.current?.positions.find(
pos => pos.entryId === entryId
)?.positions
const hasScreenPos = Boolean(entry.screenPos)
entryViews.push({
entryId,
wrapper,
indicator,
box,
callout,
layout: layoutElement,
hasScreenPos,
height: 0,
entry,
previousPositions,
// Calculate positions for entries that are above the focused entry
calculateEntryViewPositions(
entryViewsBefore,
lineHeight,
(originalTop: number, height: number) => {
const originalBottom = originalTop + height
const bottom = Math.min(originalBottom, firstEntryTop - padding)
const top = bottom - height
firstEntryTop = top
return top
}
)
// Calculate the new top overflow
const overflowTop = Math.max(0, toolbarPaddedHeight - firstEntryTop)
// Check whether the positions of any entry have changed since the last
// layout
const positions = entryViews.map(
(entryView): EntryPositions => ({
entryId: entryView.entryId,
positions: entryView.positions,
})
} else {
debugConsole.log(
'Entry wrapper is missing indicator, box or callout, so ignoring',
wrapper
)
const positionsChanged = !positionsEqual(
previousLayoutInfoRef.current.positions,
positions
)
// Check whether the top overflow or review panel height have changed
const overflowTopChanged =
overflowTop !== previousLayoutInfoRef.current.overflowTop
const height = lastEntryBottom + navPaddedHeight
const heightChanged = height !== previousLayoutInfoRef.current.height
const isMoveRequired = positionsChanged || overflowTopChanged
// Move entries into their initial positions, if animating, avoiding
// animation until the final animated move
if (animate && isMoveRequired) {
container.classList.add('no-animate')
moveEntriesToInitialPosition(entryViews, overflowTop)
}
// Inform the editor of the new top overflow and/or height if either has
// changed
if (overflowTopChanged || heightChanged) {
window.dispatchEvent(
new CustomEvent('review-panel:event', {
detail: {
type: 'sizes',
payload: {
overflowTop,
height,
},
},
})
)
}
}
if (entryViews.length === 0) {
resetLayout()
return
}
// Do the final move
if (isMoveRequired) {
if (animate) {
container.classList.remove('no-animate')
moveEntriesToFinalPositions(entryViews, overflowTop, false)
} else {
container.classList.add('no-animate')
moveEntriesToFinalPositions(entryViews, overflowTop, true)
entryViews.sort((a, b) => a.entry.offset - b.entry.offset)
// Force reflow now to ensure that entries are moved without animation
// eslint-disable-next-line no-void
void container.offsetHeight
// Do the DOM interaction in three phases:
//
// - Apply the `display` property to all elements whose visibility has
// changed. This needs to happen first in order to measure heights.
// - Measure the height of each entry
// - Move each entry without animation to their original position
// relative to the editor content
// - Re-enable animation and position each entry
//
// The idea is to batch DOM reads and writes to avoid layout thrashing. In
// this case, the best we can do is a write phase, a read phase then a
// final write phase.
// See https://web.dev/avoid-large-complex-layouts-and-layout-thrashing/
container.classList.remove('no-animate')
}
}
// First, update display for each entry that needs it
hideOrShowEntries(entryViews)
previousLayoutInfoRef.current = {
positions,
focusedEntryIndex,
height,
overflowTop,
}
},
[entries, lineHeight, navHeight, reviewPanelOpen, toolbarHeight]
)
// Next, measure the height of each entry
for (const entryView of entryViews) {
if (entryView.hasScreenPos) {
entryView.height = entryView.layout.offsetHeight
useLayoutEffect(() => {
const callback = (event: Event) => {
const e = event as CustomEvent
if (!layoutSuspended) {
// Clear previous positions if forcing a layout
if (e.detail.force) {
previousLayoutInfoRef.current = initialLayoutInfo
}
layout(e.detail.animate)
}
}
// Calculate positions for all positioned entries, starting by calculating
// which entry to put in its desired position and anchor everything else
// around. If there is an explicitly focused entry, use that.
let focusedEntryIndex = entryViews.findIndex(view => view.entry.focused)
if (focusedEntryIndex === -1) {
// There is no explicitly focused entry, so use the focused entry from the
// previous layout. This will be the first entry in the list if there was
// no previous layout.
focusedEntryIndex = Math.min(
previousLayoutInfoRef.current.focusedEntryIndex,
entryViews.length - 1
)
// If the entry from the previous layout has no screen position, fall back
// to the first entry in the list that does.
if (!entryViews[focusedEntryIndex].hasScreenPos) {
focusedEntryIndex = entryViews.findIndex(view => view.hasScreenPos)
}
window.addEventListener('review-panel:layout', callback)
return () => {
window.removeEventListener('review-panel:layout', callback)
}
// If there is no entry with a screen position, bail out
if (focusedEntryIndex === -1) {
return
}
const focusedEntryView = entryViews[focusedEntryIndex]
// If the focused entry has no screenPos, we can't position other
// entryViews relative to it, so we position all other entryViews as
// though the focused entry is at the top and the rest follow it
const entryViewsAfter = focusedEntryView.hasScreenPos
? entryViews.slice(focusedEntryIndex + 1)
: [...entryViews]
const entryViewsBefore = focusedEntryView.hasScreenPos
? entryViews.slice(0, focusedEntryIndex).reverse() // Work through backwards, starting with the one just above
: []
debugConsole.log('focusedEntryIndex', focusedEntryIndex)
let lastEntryBottom = 0
let firstEntryTop = 0
// Put the focused entry as close as possible to where it wants to be
if (focusedEntryView.hasScreenPos) {
const focusedEntryScreenPos = focusedEntryView.entry.screenPos
const entryTop = Math.max(focusedEntryScreenPos.y, toolbarPaddedHeight)
updateEntryPositions(focusedEntryView, entryTop, lineHeight)
lastEntryBottom = entryTop + focusedEntryView.height
firstEntryTop = entryTop
}
// Calculate positions for entries that are below the focused entry
calculateEntryViewPositions(
entryViewsAfter,
lineHeight,
(originalTop: number, height: number) => {
const top = Math.max(originalTop, lastEntryBottom + padding)
lastEntryBottom = top + height
return top
}
)
// Calculate positions for entries that are above the focused entry
calculateEntryViewPositions(
entryViewsBefore,
lineHeight,
(originalTop: number, height: number) => {
const originalBottom = originalTop + height
const bottom = Math.min(originalBottom, firstEntryTop - padding)
const top = bottom - height
firstEntryTop = top
return top
}
)
// Calculate the new top overflow
const overflowTop = Math.max(0, toolbarPaddedHeight - firstEntryTop)
// Check whether the positions of any entry have changed since the last
// layout
const positions = entryViews.map(
(entryView): EntryPositions => ({
entryId: entryView.entryId,
positions: entryView.positions,
})
)
const positionsChanged = !positionsEqual(
previousLayoutInfoRef.current.positions,
positions
)
// Check whether the top overflow or review panel height have changed
const overflowTopChanged =
overflowTop !== previousLayoutInfoRef.current.overflowTop
const height = lastEntryBottom + navPaddedHeight
const heightChanged = height !== previousLayoutInfoRef.current.height
const isMoveRequired = positionsChanged || overflowTopChanged
// Move entries into their initial positions, if animating, avoiding
// animation until the final animated move
if (animate && isMoveRequired) {
container.classList.add('no-animate')
moveEntriesToInitialPosition(entryViews, overflowTop)
}
// Inform the editor of the new top overflow and/or height if either has
// changed
if (overflowTopChanged || heightChanged) {
window.dispatchEvent(
new CustomEvent('review-panel:event', {
detail: {
type: 'sizes',
payload: {
overflowTop,
height,
},
},
})
)
}
// Do the final move
if (isMoveRequired) {
if (animate) {
container.classList.remove('no-animate')
moveEntriesToFinalPositions(entryViews, overflowTop, false)
} else {
container.classList.add('no-animate')
moveEntriesToFinalPositions(entryViews, overflowTop, true)
// Force reflow now to ensure that entries are moved without animation
// eslint-disable-next-line no-void
void container.offsetHeight
container.classList.remove('no-animate')
}
}
previousLayoutInfoRef.current = {
positions,
focusedEntryIndex,
height,
overflowTop,
}
}
useEventListener('review-panel:layout', (event: CustomEvent) => {
if (!layoutSuspended) {
// Clear previous positions if forcing a layout
if (event.detail.force) {
previousLayoutInfoRef.current = initialLayoutInfo
}
layout(event.detail.animate)
}
})
}, [layoutSuspended, layout])
// Layout on first render. This is necessary to ensure layout happens when
// switching from overview to current file view

View file

@ -1,5 +1,8 @@
import { useTranslation } from 'react-i18next'
import { useReviewPanelUpdaterFnsContext } from '../../context/review-panel/review-panel-context'
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 Toggler() {
const { t } = useTranslation()
@ -18,6 +21,12 @@ function Toggler() {
onClick={handleTogglerClick}
>
<span className="sr-only">{t('hotkey_toggle_review_panel')}</span>
<span className="review-panel-toggler-icon">
<BootstrapVersionSwitcher
bs3={<Icon type="chevron-left" />}
bs5={<MaterialIcon type="chevron_left" />}
/>
</span>
</button>
)
}

View file

@ -1,7 +1,6 @@
import { useTranslation } from 'react-i18next'
import { useState, useMemo, useCallback } from 'react'
import Icon from '../../../../../shared/components/icon'
import Tooltip from '../../../../../shared/components/tooltip'
import ResolvedCommentsScroller from './resolved-comments-scroller'
import classnames from 'classnames'
import {
@ -16,6 +15,10 @@ import { ReviewPanelResolvedCommentThread } from '../../../../../../../types/rev
import { DocId } from '../../../../../../../types/project-settings'
import { ReviewPanelEntry } from '../../../../../../../types/review-panel/entry'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import LoadingSpinner from '@/shared/components/loading-spinner'
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'
export interface FilteredResolvedComments
extends ReviewPanelResolvedCommentThread {
@ -97,7 +100,7 @@ function ResolvedCommentsDropdown() {
onClick={() => setIsOpen(false)}
/>
<Tooltip
<OLTooltip
id="resolved-comments-toggle"
description={t('resolved_comments')}
overlayProps={{ container: document.body, placement: 'bottom' }}
@ -107,9 +110,12 @@ function ResolvedCommentsDropdown() {
onClick={handleResolvedCommentsClick}
aria-label={t('resolved_comments')}
>
<Icon type="inbox" />
<BootstrapVersionSwitcher
bs3={<Icon type="inbox" />}
bs5={<MaterialIcon type="inbox" />}
/>
</button>
</Tooltip>
</OLTooltip>
<div
className={classnames('resolved-comments-dropdown', {
@ -117,9 +123,7 @@ function ResolvedCommentsDropdown() {
})}
>
{isLoading ? (
<div className="rp-loading">
<Icon type="spinner" spin />
</div>
<LoadingSpinner className="d-flex justify-content-center my-2" />
) : isOpen ? (
<ResolvedCommentsScroller
resolvedComments={filteredResolvedComments}

View file

@ -31,7 +31,7 @@ function ResolvedCommentsScroller({
/>
))}
{!resolvedComments.length && (
<div className="rp-loading">{t('no_resolved_threads')}</div>
<div className="text-center p-1">{t('no_resolved_threads')}</div>
)}
</div>
)

View file

@ -10,6 +10,8 @@ import {
} from '../../../context/review-panel/review-panel-context'
import { send, sendMB } from '../../../../../infrastructure/event-tracking'
import classnames from 'classnames'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
const sendAnalytics = () => {
send('subscription-funnel', 'editor-click-feature', 'real-time-track-changes')
@ -39,7 +41,7 @@ function ToggleMenu() {
<span className="review-panel-toolbar-label">
{wantTrackChanges && (
<span className="review-panel-toolbar-icon-on">
<Icon type="circle" />
<span className="track-changes-indicator-circle" />
</span>
)}
@ -47,13 +49,18 @@ function ToggleMenu() {
className="review-panel-toolbar-collapse-button"
onClick={handleToggleFullTCStateCollapse}
>
{wantTrackChanges ? <TrackChangesOn /> : <TrackChangesOff />}
<span>
{wantTrackChanges ? <TrackChangesOn /> : <TrackChangesOff />}
</span>
<span
className={classnames('rp-tc-state-collapse', {
'rp-tc-state-collapse-on': shouldCollapse,
})}
>
<Icon type="angle-down" />
<BootstrapVersionSwitcher
bs3={<Icon type="angle-down" />}
bs5={<MaterialIcon type="expand_more" />}
/>
</span>
</button>
</span>

View file

@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next'
import Tooltip from '@/shared/components/tooltip'
import TrackChangesToggle from '@/features/source-editor/components/review-panel/toolbar/track-changes-toggle'
import {
useReviewPanelUpdaterFnsContext,
@ -7,6 +6,7 @@ import {
} from '@/features/source-editor/context/review-panel/review-panel-context'
import { useProjectContext } from '@/shared/context/project-context'
import classnames from 'classnames'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
function TrackChangesMenu() {
const { t } = useTranslation()
@ -28,7 +28,7 @@ function TrackChangesMenu() {
return (
<ul className="rp-tc-state" data-testid="review-panel-track-changes-menu">
<li className="rp-tc-state-item rp-tc-state-item-everyone">
<Tooltip
<OLTooltip
description={t('tc_switch_everyone_tip')}
id="track-changes-switch-everyone"
overlayProps={{
@ -38,7 +38,7 @@ function TrackChangesMenu() {
}}
>
<span className="rp-tc-state-item-name">{t('tc_everyone')}</span>
</Tooltip>
</OLTooltip>
<TrackChangesToggle
id="track-changes-everyone"
@ -52,7 +52,7 @@ function TrackChangesMenu() {
</li>
{Object.values(formattedProjectMembers).map(member => (
<li className="rp-tc-state-item" key={member.id}>
<Tooltip
<OLTooltip
description={t('tc_switch_user_tip')}
id="track-changes-switch-user"
overlayProps={{
@ -68,7 +68,7 @@ function TrackChangesMenu() {
>
{member.name}
</span>
</Tooltip>
</OLTooltip>
<TrackChangesToggle
id={`track-changes-user-toggle-${member.id}`}
@ -90,7 +90,7 @@ function TrackChangesMenu() {
))}
<li className="rp-tc-state-separator" />
<li className="rp-tc-state-item">
<Tooltip
<OLTooltip
description={t('tc_switch_guests_tip')}
id="track-changes-switch-guests"
overlayProps={{
@ -106,7 +106,7 @@ function TrackChangesMenu() {
>
{t('tc_guests')}
</span>
</Tooltip>
</OLTooltip>
<TrackChangesToggle
id="track-changes-guests-toggle"

View file

@ -1,12 +1,21 @@
import { useTranslation } from 'react-i18next'
import { Row, Col, Button, Modal } from 'react-bootstrap'
import AccessibleModal from '../../../../shared/components/accessible-modal'
import Icon from '../../../../shared/components/icon'
import { useProjectContext } from '../../../../shared/context/project-context'
import { useUserContext } from '../../../../shared/context/user-context'
import { startFreeTrial, upgradePlan } from '../../../../main/account-upgrade'
import { memo } from 'react'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
type UpgradeTrackChangesModalProps = {
show: boolean
@ -24,11 +33,11 @@ function UpgradeTrackChangesModal({
const hasNewPaywallCta = useFeatureFlag('paywall-cta')
return (
<AccessibleModal show={show} onHide={() => setShow(false)}>
<Modal.Header closeButton>
<h3>{t('upgrade_to_track_changes')}</h3>
</Modal.Header>
<Modal.Body>
<OLModal show={show} onHide={() => setShow(false)}>
<OLModalHeader closeButton>
<OLModalTitle>{t('upgrade_to_track_changes')}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<div className="teaser-video-container">
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video className="teaser-video" autoPlay loop>
@ -45,8 +54,8 @@ function UpgradeTrackChangesModal({
<h4 className="teaser-title">
{t('see_changes_in_your_documents_live')}
</h4>
<Row>
<Col md={10} mdOffset={1}>
<OLRow>
<OLCol lg={{ span: 10, offset: 1 }}>
<ul className="list-unstyled">
{[
t('track_any_change_in_real_time'),
@ -54,36 +63,43 @@ function UpgradeTrackChangesModal({
t('accept_or_reject_each_changes_individually'),
].map(translation => (
<li key={translation}>
<Icon type="check" /> {translation}
<BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={
<MaterialIcon
type="check"
className="align-text-bottom"
/>
}
/>
&nbsp;{translation}
</li>
))}
</ul>
</Col>
</Row>
</OLCol>
</OLRow>
<p className="small">
{t('already_subscribed_try_refreshing_the_page')}
</p>
{project.owner && (
<Row className="text-center">
<div className="text-center">
{project.owner._id === user.id ? (
user.allowedFreeTrial ? (
<Button
bsStyle={null}
className="btn-primary"
<OLButton
variant="primary"
onClick={() => startFreeTrial('track-changes')}
>
{hasNewPaywallCta
? t('get_track_changes')
: t('try_it_for_free')}
</Button>
</OLButton>
) : (
<Button
bsStyle={null}
className="btn-primary"
<OLButton
variant="primary"
onClick={() => upgradePlan('project-sharing')}
>
{t('upgrade')}
</Button>
</OLButton>
)
) : (
<p>
@ -94,19 +110,15 @@ function UpgradeTrackChangesModal({
</strong>
</p>
)}
</Row>
</div>
)}
</Modal.Body>
<Modal.Footer>
<Button
bsStyle={null}
className="btn-secondary"
onClick={() => setShow(false)}
>
</OLModalBody>
<OLModalFooter>
<OLButton variant="secondary" onClick={() => setShow(false)}>
{t('close')}
</Button>
</Modal.Footer>
</AccessibleModal>
</OLButton>
</OLModalFooter>
</OLModal>
)
}

View file

@ -39,10 +39,7 @@ function LoadingSpinner({
}
const extraClasses = isBootstrap5()
? [
'd-inline-flex',
align === 'left' ? 'align-items-start' : 'align-items-center',
]
? [align === 'left' ? 'align-items-start' : 'align-items-center']
: null
return (

View file

@ -9,10 +9,8 @@
@rp-border-grey: #d9d9d9;
@rp-green: #2c8e30;
@rp-green-on-dark: rgba(37, 107, 41, 0.5);
@rp-red: #c5060b;
@rp-yellow: #f3b111;
@rp-yellow-on-dark: rgba(194, 93, 11, 0.5);
@rp-grey: #aaaaaa;
@rp-type-blue: #6b7797;
@ -24,8 +22,6 @@
@review-panel-width: 230px;
@review-off-width: 22px;
@rp-toolbar-height: 32px;
@rp-entry-animation-speed: 0.3s;
.rp-button() {
@ -64,12 +60,6 @@
}
}
.loading-panel {
.rp-size-expanded & {
right: @review-panel-width;
}
}
.review-panel-toolbar {
display: none;
@ -105,10 +95,6 @@
margin-right: 5px;
}
.review-panel-toolbar-spinner {
margin-left: 5px;
}
.rp-tc-state {
position: absolute;
top: 100%;
@ -173,10 +159,6 @@
}
}
.rp-entry-list-inner {
position: relative;
}
.rp-entry-indicator {
display: none;
z-index: 2; // above .review-panel-toggler
@ -752,11 +734,6 @@
}
}
.rp-loading {
text-align: center;
padding: 5px;
}
.rp-nav {
display: none;
flex-shrink: 0;
@ -806,18 +783,6 @@
font-size: @rp-base-font-size;
}
#editor {
.rp-size-mini & {
right: @review-off-width;
}
.rp-size-expanded & {
right: @review-panel-width;
left: 0px;
width: auto;
}
}
.review-icon {
display: inline-block;
background: url('../../../../public/img/ol-icons/review-icon-dark-theme.svg')
@ -919,6 +884,7 @@ button when (@is-overleaf-light = true) {
}
.rp-collapse-toggle {
.btn-inline-link;
color: @rp-type-blue;
font-weight: @rp-semibold-weight;
@ -979,8 +945,8 @@ button when (@is-overleaf-light = true) {
}
.rp-size-expanded & {
&::after {
content: '\f105';
.review-panel-toggler-icon .fa {
transform: scale(0.7) rotate(180deg);
}
}
@ -990,20 +956,16 @@ button when (@is-overleaf-light = true) {
background-color: #fff;
}
&::after {
content: '\f104';
font-family: FontAwesome;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 16px;
display: block;
.review-panel-toggler-icon {
position: sticky;
top: 50%;
bottom: 50%;
width: 100%;
text-align: center;
margin-top: -0.5em;
.fa {
font-size: 14px;
transform: scale(0.7);
}
}
}
@ -1134,40 +1096,10 @@ button when (@is-overleaf-light = true) {
}
}
.review-panel-indicator {
display: none;
}
.review-panel-mini {
width: 22px !important;
overflow: visible !important;
.rp-entry {
height: 12px;
}
.rp-entry-indicator {
position: absolute;
left: -6px;
right: 0;
display: flex;
width: 20px;
height: 20px;
}
.rp-entry-content {
display: none;
background: white;
border: 1px solid @rp-border-grey;
width: 200px;
}
.rp-entry:hover {
.rp-entry-content {
display: initial;
position: absolute;
left: -206px;
top: 0;
}
}
.track-changes-indicator-circle {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 100%;
background-color: @ol-green;
}

View file

@ -143,3 +143,33 @@
--link-hover-color: var(--link-hover-color-dark);
--link-visited-color: var(--link-visited-color-dark);
}
@mixin triangle($direction, $width, $height, $color) {
position: absolute;
border-color: transparent;
border-style: solid;
width: 0;
height: 0;
@if $direction == top {
border-width: 0 calc(#{$width} / 2) #{$height} calc(#{$width} / 2);
border-bottom-color: $color;
border-left-color: transparent;
border-right-color: transparent;
} @else if $direction == bottom {
border-width: #{$height} calc(#{$width} / 2) 0 calc(#{$width} / 2);
border-top-color: $color;
border-left-color: transparent;
border-right-color: transparent;
} @else if $direction == right {
border-width: calc(#{$height} / 2) 0 calc(#{$height} / 2) #{$width};
border-left-color: $color;
border-top-color: transparent;
border-bottom-color: transparent;
} @else if $direction == left {
border-width: calc(#{$height} / 2) #{$width} calc(#{$height} / 2) 0;
border-right-color: $color;
border-top-color: transparent;
border-bottom-color: transparent;
}
}

View file

@ -1,4 +1,6 @@
.loading {
display: inline-flex;
.spinner-border-sm,
.spinner-border {
// Ensure the thickness of the spinner is independent of the font size of its container

View file

@ -294,8 +294,8 @@ $editor-toggler-bg-dark-color: color.adjust(
}
.teaser-video-container {
margin: calc(var(--spacing-07) * -1) calc(var(--spacing-07) * -1)
var(--spacing-02) calc(var(--spacing-07) * -1);
margin: calc(var(--bs-modal-padding) * -1) calc(var(--bs-modal-padding) * -1)
var(--spacing-02) calc(var(--bs-modal-padding) * -1);
overflow: hidden;
}

View file

@ -84,3 +84,11 @@
position: absolute;
z-index: 1;
}
.d-flex {
display: flex !important;
}
.justify-content-center {
justify-content: center !important;
}