Просмотр исходного кода

Merge pull request #20008 from overleaf/ae-review-panel-empty-state

Improve calculations of empty state, mini state and sizes variables in review panel

GitOrigin-RevId: 41bcb3b67c9f0019c11b4de0e4590b0407e04e66
Alf Eaton 2 лет назад
Родитель
Сommit
e61eb1b220

+ 1 - 2
services/web/frontend/js/features/review-panel-new/components/review-panel-add-comment.tsx

@@ -4,7 +4,6 @@ import {
   useCodeMirrorViewContext,
 } from '@/features/source-editor/components/codemirror-editor'
 import { EditorSelection } from '@codemirror/state'
-import { PANEL_WIDTH } from './review-panel'
 import { useTranslation } from 'react-i18next'
 import { useThreadsActionsContext } from '../context/threads-context'
 
@@ -49,7 +48,7 @@ export const ReviewPanelAddComment: FC = () => {
   return (
     <form
       onSubmit={handleSubmit}
-      style={{ display: 'flex', flexDirection: 'column', width: PANEL_WIDTH }}
+      className="review-panel-add-comment-form"
       ref={handleElement}
     >
       {/* eslint-disable-next-line jsx-a11y/no-autofocus */}

+ 15 - 4
services/web/frontend/js/features/review-panel-new/components/review-panel-container.tsx

@@ -4,19 +4,30 @@ import { memo } from 'react'
 import ReviewPanel from './review-panel'
 import { useLayoutContext } from '@/shared/context/layout-context'
 import { useRangesContext } from '../context/ranges-context'
+import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
+import { hasActiveRange } from '@/features/review-panel-new/utils/has-active-range'
 
 function ReviewPanelContainer() {
   const view = useCodeMirrorViewContext()
   const ranges = useRangesContext()
+  const threads = useThreadsContext()
   const { reviewPanelOpen } = useLayoutContext()
 
-  const mini = !reviewPanelOpen && !!ranges?.total
-
-  if (!view || (!reviewPanelOpen && !mini)) {
+  if (!view) {
     return null
   }
 
-  return ReactDOM.createPortal(<ReviewPanel mini={mini} />, view.scrollDOM)
+  // the full-width review panel
+  if (reviewPanelOpen) {
+    return ReactDOM.createPortal(<ReviewPanel />, view.scrollDOM)
+  }
+
+  // the mini review panel
+  if (hasActiveRange(ranges, threads)) {
+    return ReactDOM.createPortal(<ReviewPanel mini />, view.scrollDOM)
+  }
+
+  return null
 }
 
 export default memo(ReviewPanelContainer)

+ 101 - 101
services/web/frontend/js/features/review-panel-new/components/review-panel-current-file.tsx

@@ -16,7 +16,6 @@ import {
   DeleteOperation,
   EditOperation,
 } from '../../../../../types/change'
-import { editorVerticalTopPadding } from '@/features/source-editor/extensions/vertical-overflow'
 import {
   useCodeMirrorStateContext,
   useCodeMirrorViewContext,
@@ -27,18 +26,14 @@ import { isDeleteChange, isInsertChange } from '@/utils/operations'
 import Icon from '@/shared/components/icon'
 import { positionItems } from '../utils/position-items'
 import { canAggregate } from '../utils/can-aggregate'
-import { isInViewport } from '../utils/is-in-viewport'
 import ReviewPanelEmptyState from './review-panel-empty-state'
 import useEventListener from '@/shared/hooks/use-event-listener'
+import { hasActiveRange } from '@/features/review-panel-new/utils/has-active-range'
 
-type Positions = Map<string, number>
-type Aggregates = Map<string, Change<DeleteOperation>>
-
-type RangesWithPositions = {
+type AggregatedRanges = {
   changes: Change<EditOperation>[]
   comments: Change<CommentOperation>[]
-  positions: Positions
-  aggregates: Aggregates
+  aggregates: Map<string, Change<DeleteOperation>>
 }
 
 const ReviewPanelCurrentFile: FC = () => {
@@ -47,24 +42,7 @@ const ReviewPanelCurrentFile: FC = () => {
   const threads = useThreadsContext()
   const state = useCodeMirrorStateContext()
 
-  const [rangesWithPositions, setRangesWithPositions] =
-    useState<RangesWithPositions>()
-
-  const contentRect = view.contentDOM.getBoundingClientRect()
-
-  const editorPaddingTop = editorVerticalTopPadding(view)
-  const topDiff = contentRect.top - editorPaddingTop
-  const docLength = state.doc.length
-
-  const screenPosition = useCallback(
-    (change: Change): number | undefined => {
-      const pos = Math.min(change.op.p, docLength)
-      const coords = view.coordsAtPos(pos)
-
-      return coords ? Math.round(coords.top - topDiff) : undefined
-    },
-    [docLength, topDiff, view]
-  )
+  const [aggregatedRanges, setAggregatedRanges] = useState<AggregatedRanges>()
 
   const selectionCoords = useMemo(
     () =>
@@ -85,7 +63,7 @@ const ReviewPanelCurrentFile: FC = () => {
       )
 
       if (extents) {
-        previousFocusedItem.current = extents.focusedItemIndex
+        previousFocusedItem.current = extents.activeItemIndex
       }
     }
   }, [])
@@ -124,62 +102,84 @@ const ReviewPanelCurrentFile: FC = () => {
     }
   }, [updatePositions])
 
-  const buildEntries = useCallback(() => {
+  const buildAggregatedRanges = useCallback(() => {
     if (ranges) {
+      const output: AggregatedRanges = {
+        aggregates: new Map(),
+        changes: [],
+        comments: [],
+      }
+
+      let precedingChange: Change<EditOperation> | null = null
+
+      for (const change of ranges.changes) {
+        if (
+          precedingChange &&
+          isInsertChange(precedingChange) &&
+          isDeleteChange(change) &&
+          canAggregate(change, precedingChange)
+        ) {
+          output.aggregates.set(precedingChange.id, change)
+        } else {
+          output.changes.push(change)
+        }
+
+        precedingChange = change
+      }
+
+      if (threads) {
+        for (const comment of ranges.comments) {
+          if (!threads[comment.op.t]?.resolved) {
+            output.comments.push(comment)
+          }
+        }
+      }
+
+      setAggregatedRanges(output)
+    }
+  }, [threads, ranges])
+
+  useEffect(() => {
+    buildAggregatedRanges()
+  }, [buildAggregatedRanges])
+
+  useEventListener('editor:viewport-changed', buildAggregatedRanges)
+
+  const [positions, setPositions] = useState<Map<string, number>>(new Map())
+
+  const positionsRef = useRef<Map<string, number>>(new Map())
+
+  useEffect(() => {
+    if (aggregatedRanges) {
       view.requestMeasure({
         key: 'review-panel-position',
-        read(view): RangesWithPositions {
-          const isVisible = isInViewport(view)
-
-          const output: RangesWithPositions = {
-            positions: new Map(),
-            aggregates: new Map(),
-            changes: [],
-            comments: [],
-          }
+        read(view) {
+          const contentRect = view.contentDOM.getBoundingClientRect()
+          const docLength = view.state.doc.length
 
-          let precedingChange: Change<EditOperation> | null = null
-
-          for (const change of ranges.changes) {
-            if (isVisible(change)) {
-              if (
-                precedingChange &&
-                isInsertChange(precedingChange) &&
-                isDeleteChange(change) &&
-                canAggregate(change, precedingChange)
-              ) {
-                output.aggregates.set(precedingChange.id, change)
-              } else {
-                output.changes.push(change)
-
-                const position = screenPosition(change)
-                if (position) {
-                  output.positions.set(change.id, position)
-                }
-              }
-            }
+          const screenPosition = (change: Change): number | undefined => {
+            const pos = Math.min(change.op.p, docLength) // TODO: needed?
+            const coords = view.coordsAtPos(pos)
 
-            precedingChange = change
+            return coords ? Math.round(coords.top - contentRect.top) : undefined
           }
 
-          if (threads) {
-            for (const comment of ranges.comments) {
-              if (isVisible(comment)) {
-                output.comments.push(comment)
-                if (!threads[comment.op.t]?.resolved) {
-                  const position = screenPosition(comment)
-                  if (position) {
-                    output.positions.set(comment.id, position)
-                  }
-                }
-              }
+          for (const change of aggregatedRanges.changes) {
+            const position = screenPosition(change)
+            if (position) {
+              positionsRef.current.set(change.id, position)
             }
           }
 
-          return output
+          for (const comment of aggregatedRanges.comments) {
+            const position = screenPosition(comment)
+            if (position) {
+              positionsRef.current.set(comment.id, position)
+            }
+          }
         },
-        write(positionedRanges) {
-          setRangesWithPositions(positionedRanges)
+        write() {
+          setPositions(positionsRef.current)
           window.setTimeout(() => {
             containerRef.current?.dispatchEvent(
               new Event('review-panel:position')
@@ -188,28 +188,22 @@ const ReviewPanelCurrentFile: FC = () => {
         },
       })
     }
-  }, [screenPosition, threads, view, ranges])
-
-  useEffect(() => {
-    buildEntries()
-  }, [buildEntries])
+  }, [view, aggregatedRanges])
 
-  useEventListener('editor:viewport-changed', buildEntries)
+  const showEmptyState = useMemo(
+    () => hasActiveRange(ranges, threads) === false,
+    [ranges, threads]
+  )
 
-  if (!rangesWithPositions) {
+  if (!aggregatedRanges) {
     return null
   }
 
-  const showEmptyState =
-    threads &&
-    rangesWithPositions.changes.length === 0 &&
-    rangesWithPositions.comments.length === 0
-
   return (
     <div ref={handleContainer}>
       {selectionCoords && (
         <div
-          className="review-panel-entry"
+          className="review-panel-entry review-panel-entry-action"
           style={{ position: 'absolute' }}
           data-top={selectionCoords.top + view.scrollDOM.scrollTop - 70}
           data-pos={state.selection.main.head}
@@ -225,22 +219,28 @@ const ReviewPanelCurrentFile: FC = () => {
 
       {showEmptyState && <ReviewPanelEmptyState />}
 
-      {rangesWithPositions.changes.map(change => (
-        <ReviewPanelChange
-          key={change.id}
-          change={change}
-          top={rangesWithPositions.positions.get(change.id)}
-          aggregate={rangesWithPositions.aggregates.get(change.id)}
-        />
-      ))}
-
-      {rangesWithPositions.comments.map(comment => (
-        <ReviewPanelComment
-          key={comment.id}
-          comment={comment}
-          top={rangesWithPositions.positions.get(comment.id)}
-        />
-      ))}
+      {aggregatedRanges.changes.map(
+        change =>
+          positions.has(change.id) && (
+            <ReviewPanelChange
+              key={change.id}
+              change={change}
+              top={positions.get(change.id)}
+              aggregate={aggregatedRanges.aggregates.get(change.id)}
+            />
+          )
+      )}
+
+      {aggregatedRanges.comments.map(
+        comment =>
+          positions.has(comment.id) && (
+            <ReviewPanelComment
+              key={comment.id}
+              comment={comment}
+              top={positions.get(comment.id)}
+            />
+          )
+      )}
     </div>
   )
 }

+ 2 - 5
services/web/frontend/js/features/review-panel-new/components/review-panel-header.tsx

@@ -6,16 +6,13 @@ import { Button } from 'react-bootstrap'
 import MaterialIcon from '@/shared/components/material-icon'
 import { useLayoutContext } from '@/shared/context/layout-context'
 
-const ReviewPanelHeader: FC<{
-  top: number
-  width: number
-}> = ({ top, width }) => {
+const ReviewPanelHeader: FC = () => {
   const [trackChangesMenuExpanded, setTrackChangesMenuExpanded] =
     useState(false)
   const { setReviewPanelOpen } = useLayoutContext()
 
   return (
-    <div className="review-panel-header" style={{ top, width }}>
+    <div className="review-panel-header">
       <div className="review-panel-heading">
         <div className="review-panel-label">Review</div>
         <Button

+ 4 - 8
services/web/frontend/js/features/review-panel-new/components/review-panel-overview.tsx

@@ -1,4 +1,4 @@
-import { FC, useMemo } from 'react'
+import { FC, Fragment, useMemo } from 'react'
 import { useFileTreeData } from '@/shared/context/file-tree-data-context'
 import { Ranges, useRangesContext } from '../context/ranges-context'
 import { useTranslation } from 'react-i18next'
@@ -59,14 +59,10 @@ export const ReviewPanelOverview: FC = () => {
             const ranges = rangesForDocs.get(doc.doc.id)
             return (
               ranges && (
-                <>
-                  <ReviewPanelOverviewFile
-                    key={doc.doc.id}
-                    doc={doc}
-                    ranges={ranges}
-                  />
+                <Fragment key={doc.doc.id}>
+                  <ReviewPanelOverviewFile doc={doc} ranges={ranges} />
                   <div className="review-panel-overfile-divider" />
-                </>
+                </Fragment>
               )
             )
           })}

+ 12 - 49
services/web/frontend/js/features/review-panel-new/components/review-panel.tsx

@@ -1,69 +1,32 @@
 import { FC, memo, useState } from 'react'
-import {
-  useCodeMirrorStateContext,
-  useCodeMirrorViewContext,
-} from '@/features/source-editor/components/codemirror-editor'
 import ReviewPanelTabs from './review-panel-tabs'
 import ReviewPanelHeader from './review-panel-header'
 import ReviewPanelCurrentFile from './review-panel-current-file'
 import { ReviewPanelOverview } from './review-panel-overview'
 import classnames from 'classnames'
+import { useReviewPanelStyles } from '@/features/review-panel-new/hooks/use-review-panel-styles'
 
 export type SubView = 'cur_file' | 'overview'
 
-export const PANEL_WIDTH = 230
-export const PANEL_MINI_WIDTH = 20
-
-const ReviewPanel: FC<{ mini: boolean }> = ({ mini }) => {
-  const view = useCodeMirrorViewContext()
-  // eslint-disable-next-line no-unused-vars
-  const _state = useCodeMirrorStateContext() // needs to update on editor state changes
-
+const ReviewPanel: FC<{ mini?: boolean }> = ({ mini = false }) => {
   const [subView, setSubView] = useState<SubView>('cur_file')
 
-  const contentRect = view.contentDOM.getBoundingClientRect()
-  const scrollRect = view.scrollDOM.getBoundingClientRect()
+  const style = useReviewPanelStyles(mini)
+
+  const className = classnames('review-panel-new', 'review-panel-container', {
+    'review-panel-mini': mini,
+    'review-panel-subview-overview': subView === 'overview',
+  })
 
   return (
-    <div
-      className="review-panel-container"
-      style={{
-        overflowY: subView === 'overview' ? 'hidden' : undefined,
-        position: subView === 'overview' ? 'sticky' : 'relative',
-        top: subView === 'overview' ? 0 : undefined,
-      }}
-    >
-      <div
-        className={classnames('review-panel-new', {
-          'review-panel-mini': mini,
-        })}
-        style={{
-          flexShrink: 0,
-          minHeight: subView === 'cur_file' ? contentRect.height : 'auto',
-          height: subView === 'overview' ? '100%' : undefined,
-          overflow: subView === 'overview' ? 'hidden' : undefined,
-          width: mini ? PANEL_MINI_WIDTH : PANEL_WIDTH,
-        }}
-      >
-        {!mini && (
-          <ReviewPanelHeader top={scrollRect.top - 40} width={PANEL_WIDTH} />
-        )}
+    <div className={className} style={style}>
+      <div className="review-panel-inner">
+        {!mini && <ReviewPanelHeader />}
 
         {subView === 'cur_file' && <ReviewPanelCurrentFile />}
         {subView === 'overview' && <ReviewPanelOverview />}
 
-        <div
-          className="review-panel-footer"
-          style={{
-            position: 'fixed',
-            top: scrollRect.bottom - 66,
-            zIndex: 1,
-            background: '#fafafa',
-            borderTop: 'solid 1px #d9d9d9',
-            width: PANEL_WIDTH,
-            display: mini ? 'none' : 'flex',
-          }}
-        >
+        <div className="review-panel-footer">
           <ReviewPanelTabs subView={subView} setSubView={setSubView} />
         </div>
       </div>

+ 54 - 0
services/web/frontend/js/features/review-panel-new/hooks/use-review-panel-styles.ts

@@ -0,0 +1,54 @@
+import { CSSProperties, useCallback, useEffect, useState } from 'react'
+import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-editor'
+
+export const useReviewPanelStyles = (mini: boolean) => {
+  const view = useCodeMirrorViewContext()
+  const [styles, setStyles] = useState<CSSProperties>()
+
+  const updateScrollDomVariables = useCallback((element: HTMLDivElement) => {
+    const { top, bottom } = element.getBoundingClientRect()
+
+    setStyles(value => ({
+      ...value,
+      '--review-panel-top': `${top}px`,
+      '--review-panel-bottom': `${bottom}px`,
+    }))
+  }, [])
+
+  const updateContentDomVariables = useCallback((element: HTMLDivElement) => {
+    const { height } = element.getBoundingClientRect()
+
+    setStyles(value => ({
+      ...value,
+      '--review-panel-height': `${height}px`,
+    }))
+  }, [])
+
+  useEffect(() => {
+    setStyles(value => ({
+      ...value,
+      '--review-panel-width': mini ? '22px' : '230px',
+    }))
+  }, [mini])
+
+  useEffect(() => {
+    if ('ResizeObserver' in window) {
+      const scrollDomObserver = new window.ResizeObserver(entries =>
+        updateScrollDomVariables(entries[0]?.target as HTMLDivElement)
+      )
+      scrollDomObserver.observe(view.scrollDOM)
+
+      const contentDomObserver = new window.ResizeObserver(entries =>
+        updateContentDomVariables(entries[0]?.target as HTMLDivElement)
+      )
+      contentDomObserver.observe(view.contentDOM)
+
+      return () => {
+        scrollDomObserver.disconnect()
+        contentDomObserver.disconnect()
+      }
+    }
+  }, [view, updateScrollDomVariables, updateContentDomVariables])
+
+  return styles
+}

+ 25 - 0
services/web/frontend/js/features/review-panel-new/utils/has-active-range.ts

@@ -0,0 +1,25 @@
+import { Ranges } from '@/features/review-panel-new/context/ranges-context'
+import { Threads } from '@/features/review-panel-new/context/threads-context'
+
+export const hasActiveRange = (
+  ranges: Ranges | undefined,
+  threads: Threads | undefined
+): boolean | undefined => {
+  if (!ranges || !threads) {
+    // data isn't loaded yet
+    return undefined
+  }
+
+  if (ranges.changes.length > 0) {
+    // at least one tracked change
+    return true
+  }
+
+  for (const thread of Object.values(threads)) {
+    if (!thread.resolved) {
+      return true
+    }
+  }
+
+  return false
+}

+ 0 - 7
services/web/frontend/js/features/review-panel-new/utils/is-in-viewport.tsx

@@ -1,7 +0,0 @@
-import { EditorView } from '@codemirror/view'
-import { Change } from '../../../../../types/change'
-
-export const isInViewport =
-  (view: EditorView) =>
-  (change: Change): boolean =>
-    change.op.p >= view.viewport.from && change.op.p <= view.viewport.to

+ 29 - 19
services/web/frontend/js/features/review-panel-new/utils/position-items.ts

@@ -15,34 +15,44 @@ export const positionItems = debounce(
       return
     }
 
-    let focusedItemIndex = items.findIndex(item =>
-      item.classList.contains('review-panel-entry-focused')
+    let activeItemIndex = items.findIndex(item =>
+      item.classList.contains('review-panel-entry-action')
     )
-    if (focusedItemIndex === -1) {
+
+    if (activeItemIndex === -1) {
+      // if there is no action available
+      // check if there is a focused entry
+      activeItemIndex = items.findIndex(item =>
+        item.classList.contains('review-panel-entry-focused')
+      )
+    }
+
+    if (activeItemIndex === -1) {
       // if entry was not focused manually
       // check if there is an entry in selection and use that as the focused item
-      focusedItemIndex = items.findIndex(item =>
+      activeItemIndex = items.findIndex(item =>
         item.classList.contains('review-panel-entry-highlighted')
       )
     }
-    if (focusedItemIndex === -1) {
-      focusedItemIndex = previousFocusedItemIndex
+
+    if (activeItemIndex === -1) {
+      activeItemIndex = previousFocusedItemIndex
     }
 
-    const focusedItem = items[focusedItemIndex]
-    if (!focusedItem) {
+    const activeItem = items[activeItemIndex]
+    if (!activeItem) {
       return
     }
 
-    const focusedItemTop = getTopPosition(focusedItem, focusedItemIndex === 0)
+    const activeItemTop = getTopPosition(activeItem, activeItemIndex === 0)
 
-    focusedItem.style.top = `${focusedItemTop}px`
-    focusedItem.style.visibility = 'visible'
-    const focusedItemRect = focusedItem.getBoundingClientRect()
+    activeItem.style.top = `${activeItemTop}px`
+    activeItem.style.visibility = 'visible'
+    const focusedItemRect = activeItem.getBoundingClientRect()
 
-    // above the focused item
-    let topLimit = focusedItemTop
-    for (let i = focusedItemIndex - 1; i >= 0; i--) {
+    // above the active item
+    let topLimit = activeItemTop
+    for (let i = activeItemIndex - 1; i >= 0; i--) {
       const item = items[i]
       const rect = item.getBoundingClientRect()
       let top = getTopPosition(item, i === 0)
@@ -55,9 +65,9 @@ export const positionItems = debounce(
       topLimit = top
     }
 
-    // below the focused item
-    let bottomLimit = focusedItemTop + focusedItemRect.height
-    for (let i = focusedItemIndex + 1; i < items.length; i++) {
+    // below the active item
+    let bottomLimit = activeItemTop + focusedItemRect.height
+    for (let i = activeItemIndex + 1; i < items.length; i++) {
       const item = items[i]
       const rect = item.getBoundingClientRect()
       let top = getTopPosition(item, false)
@@ -70,7 +80,7 @@ export const positionItems = debounce(
     }
 
     return {
-      focusedItemIndex,
+      activeItemIndex,
       min: topLimit,
       max: bottomLimit,
     }

+ 94 - 46
services/web/frontend/stylesheets/app/editor/review-panel-new.less

@@ -1,17 +1,22 @@
-.review-panel-container {
-  height: 100%;
-  flex-shrink: 0;
-}
-
 .review-panel-new {
-  z-index: 6;
-  flex-shrink: 0;
-  background-color: @neutral-10;
-  border-left: solid 0 @neutral-20;
-  font-family: @font-family-base;
-  line-height: @line-height-base;
-  font-size: @font-size-01;
-  box-sizing: content-box;
+  &.review-panel-container {
+    height: 100%;
+    flex-shrink: 0;
+    position: relative;
+  }
+
+  .review-panel-inner {
+    z-index: 6;
+    flex-shrink: 0;
+    background-color: @neutral-10;
+    border-left: solid 0 @neutral-20;
+    font-family: @font-family-base;
+    line-height: @line-height-base;
+    font-size: @font-size-01;
+    box-sizing: content-box;
+    width: var(--review-panel-width);
+    min-height: var(--review-panel-height);
+  }
 
   .review-panel-entry {
     background-color: white;
@@ -38,6 +43,7 @@
     margin-left: @spacing-01;
     border: 1px solid @blue-50;
   }
+
   .review-panel-entry-header {
     display: flex;
     justify-content: space-between;
@@ -46,9 +52,11 @@
       color: @blue;
       font-size: 110%;
     }
+
     .review-panel-entry-time {
       color: @content-secondary;
     }
+
     .review-panel-entry-actions {
       display: flex;
       align-items: center;
@@ -73,16 +81,19 @@
       }
     }
   }
+
   .review-panel-change-body {
     display: flex;
     align-items: center;
     color: @content-secondary;
     gap: @spacing-02;
   }
+
   .review-panel-content-highlight {
     color: @content-primary;
     text-decoration: none;
   }
+
   del.review-panel-content-highlight {
     text-decoration: line-through;
   }
@@ -92,14 +103,17 @@
     padding: @spacing-02;
     font-size: 16px;
   }
+
   .review-panel-entry-icon-accept {
     background-color: @green-10;
     color: @green-50;
   }
+
   .review-panel-entry-icon-reject {
     background-color: @red-10;
     color: @red-50;
   }
+
   .review-panel-entry-icon-changed {
     background-color: @neutral-20;
     color: @content-secondary;
@@ -107,7 +121,8 @@
 
   .review-panel-header {
     position: fixed;
-    top: 0;
+    top: calc(var(--review-panel-top) - 40px);
+    width: var(--review-panel-width);
     z-index: 2;
     display: flex;
     flex-direction: column;
@@ -291,19 +306,22 @@
     display: flex;
     gap: @spacing-04;
   }
+
   .review-panel-comment {
     flex-grow: 1;
   }
+
   .review-panel-comment-reply-divider {
     border-left: 2px solid @yellow-20;
   }
+
   .review-panel-comment-body {
     font-size: @font-size-02;
     color: @content-primary;
     overflow-wrap: anywhere;
   }
 
-  .review-panel-content-expandable  {
+  .review-panel-content-expandable {
     display: -webkit-box;
     text-overflow: ellipsis;
     -webkit-box-orient: vertical;
@@ -311,6 +329,7 @@
     line-clamp: 3;
     overflow: hidden;
   }
+
   .review-panel-content-expanded {
     display: block;
   }
@@ -330,6 +349,12 @@
     max-height: 400px;
   }
 
+  .review-panel-add-comment-form {
+    display: flex;
+    flex-direction: column;
+    width: var(--review-panel-width);
+  }
+
   .review-panel-empty-state {
     position: absolute;
     top: 0;
@@ -419,46 +444,69 @@
   }
 
   .review-panel-footer {
+    position: fixed;
+    top: calc(var(--review-panel-bottom) - 66px);
+    width: var(--review-panel-width);
     z-index: 2;
+    background: @rp-bg-dim-blue;
+    border-top: 1px solid #d9d9d9;
+    display: flex;
   }
-}
 
-.review-panel-new.review-panel-mini {
-  width: 22px !important;
-  overflow: visible !important;
+  &.review-panel-subview-overview {
+    &.review-panel-container {
+      overflow-y: hidden;
+      position: sticky;
+      top: 0;
+    }
 
-  .review-panel-entry {
-    margin-left: 2px;
-    background-color: transparent;
-    border: none;
+    .review-panel-inner {
+      min-height: auto;
+      height: 100%;
+      overflow: hidden;
+    }
   }
 
-  .review-panel-entry-indicator {
-    position: absolute;
-    left: 0;
-    top: 7px;
-    display: flex;
-    width: 16px;
-    height: 16px;
-    color: @content-secondary;
-    cursor: pointer;
-  }
+  &.review-panel-mini {
+    overflow: visible !important;
 
-  .review-panel-entry-content {
-    display: none;
-    background: white;
-    border: 1px solid @rp-border-grey;
-    border-radius: @border-radius-base-new;
-    width: 200px;
-    padding: @spacing-02;
-  }
+    .review-panel-entry {
+      margin-left: 2px;
+      background-color: transparent;
+      border: none;
+    }
 
-  .review-panel-entry:hover {
-    .review-panel-entry-content {
-      display: initial;
+    .review-panel-entry-indicator {
       position: absolute;
-      left: -200px;
-      top: 0;
+      left: 0;
+      top: 7px;
+      display: flex;
+      width: 16px;
+      height: 16px;
+      color: @content-secondary;
+      cursor: pointer;
+    }
+
+    .review-panel-entry-content {
+      display: none;
+      background: white;
+      border: 1px solid @rp-border-grey;
+      border-radius: @border-radius-base-new;
+      width: 200px;
+      padding: @spacing-02;
+    }
+
+    .review-panel-entry:hover {
+      .review-panel-entry-content {
+        display: initial;
+        position: absolute;
+        left: -200px;
+        top: 0;
+      }
+    }
+
+    .review-panel-footer {
+      display: none;
     }
   }
 }