Browse Source

ref(replay): Extract useTimelineScale from useReplayContext and tweak ui styles (#73613)

The UI tweaks come from this older figma:
https://www.figma.com/design/tlGOyIijfu309du7zXTDTM/Library%3A-Replay?node-id=0-1&t=iRFVZSPdf7cuccpS-0

Changes:
- Down Triangle for both sides
- Triangle is a little higher, so it's not cutoff by the scrubber circle
when they point to the same spot (see image below)
- Unicode `x`
- font size/weight/color changes for the current/total time labels

**After:**
<img width="704" alt="SCR-20240701-oerr"
src="https://github.com/getsentry/sentry/assets/187460/3f5152d2-bf75-468e-9cfc-a498a3aed0d1">
Ryan Albrecht 8 months ago
parent
commit
c2cb0533d2

+ 3 - 1
static/app/components/replays/breadcrumbs/replayTimeline.tsx

@@ -15,10 +15,12 @@ import {useTimelineScrubberMouseTracking} from 'sentry/components/replays/player
 import {useReplayContext} from 'sentry/components/replays/replayContext';
 import divide from 'sentry/utils/number/divide';
 import toPercent from 'sentry/utils/number/toPercent';
+import useTimelineScale from 'sentry/utils/replays/hooks/useTimelineScale';
 import {useDimensions} from 'sentry/utils/useDimensions';
 
 export default function ReplayTimeline() {
-  const {replay, currentTime, timelineScale} = useReplayContext();
+  const {replay, currentTime} = useReplayContext();
+  const [timelineScale] = useTimelineScale();
 
   const panelRef = useRef<HTMLDivElement>(null);
   const mouseTrackingProps = useTimelineScrubberMouseTracking(

+ 8 - 14
static/app/components/replays/player/scrubber.tsx

@@ -11,6 +11,7 @@ import {space} from 'sentry/styles/space';
 import formatReplayDuration from 'sentry/utils/duration/formatReplayDuration';
 import divide from 'sentry/utils/number/divide';
 import toPercent from 'sentry/utils/number/toPercent';
+import useTimelineScale from 'sentry/utils/replays/hooks/useTimelineScale';
 
 type Props = {
   className?: string;
@@ -18,8 +19,8 @@ type Props = {
 };
 
 function Scrubber({className, showZoomIndicators = false}: Props) {
-  const {replay, currentHoverTime, currentTime, setCurrentTime, timelineScale} =
-    useReplayContext();
+  const {replay, currentHoverTime, currentTime, setCurrentTime} = useReplayContext();
+  const [timelineScale] = useTimelineScale();
 
   const durationMs = replay?.getDurationMs() ?? 0;
   const percentComplete = divide(currentTime, durationMs);
@@ -44,15 +45,15 @@ function Scrubber({className, showZoomIndicators = false}: Props) {
     <Wrapper className={className}>
       {showZoomIndicators ? (
         <Fragment>
-          <ZoomIndicatorContainer style={{left: toPercent(translate()), top: '-10px'}}>
+          <ZoomIndicatorContainer style={{left: toPercent(translate())}}>
             <ZoomTriangleDown />
             <ZoomIndicator />
           </ZoomIndicatorContainer>
           <ZoomIndicatorContainer
-            style={{left: toPercent(translate() + 2 * initialTranslate), top: '-2px'}}
+            style={{left: toPercent(translate() + 2 * initialTranslate)}}
           >
+            <ZoomTriangleDown />
             <ZoomIndicator />
-            <ZoomTriangleUp />
           </ZoomIndicatorContainer>
         </Fragment>
       ) : null}
@@ -140,8 +141,9 @@ const ZoomIndicatorContainer = styled('div')`
   display: flex;
   flex-direction: column;
   align-items: center;
-  gap: ${space(0.5)};
+  gap: ${space(0.75)};
   translate: -6px;
+  top: -12px;
 `;
 
 const ZoomIndicator = styled('div')`
@@ -158,14 +160,6 @@ const ZoomTriangleDown = styled('div')`
   border-top: 4px solid ${p => p.theme.gray500};
 `;
 
-const ZoomTriangleUp = styled('div')`
-  width: 0;
-  height: 0;
-  border-left: 4px solid transparent;
-  border-right: 4px solid transparent;
-  border-bottom: 4px solid ${p => p.theme.gray500};
-`;
-
 export const TimelineScrubber = styled(Scrubber)`
   height: 100%;
 

+ 0 - 15
static/app/components/replays/replayContext.tsx

@@ -138,21 +138,11 @@ interface ReplayPlayerContextProps extends HighlightCallbacks {
    */
   setSpeed: (speed: number) => void;
 
-  /**
-   * Set the timeline width to the specific scale, starting at 1x and growing larger
-   */
-  setTimelineScale: (size: number) => void;
-
   /**
    * The speed for normal playback
    */
   speed: number;
 
-  /**
-   * Scale of the timeline width, starts from 1x and increases by 1x
-   */
-  timelineScale: number;
-
   /**
    * Start or stop playback
    *
@@ -189,9 +179,7 @@ const ReplayPlayerContext = createContext<ReplayPlayerContextProps>({
   setCurrentTime: () => {},
   setRoot: () => {},
   setSpeed: () => {},
-  setTimelineScale: () => {},
   speed: 1,
-  timelineScale: 1,
   togglePlayPause: () => {},
   toggleSkipInactive: () => {},
 });
@@ -275,7 +263,6 @@ export function Provider({
   const [isVideoBuffering, setVideoBuffering] = useState(false);
   const playTimer = useRef<number | undefined>(undefined);
   const didApplyInitialOffset = useRef(false);
-  const [timelineScale, setTimelineScale] = useState(1);
   const [rootEl, setRoot] = useState<HTMLDivElement | null>(null);
 
   const durationMs = replay?.getDurationMs() ?? 0;
@@ -721,9 +708,7 @@ export function Provider({
         setCurrentHoverTime,
         setCurrentTime,
         setSpeed,
-        setTimelineScale,
         speed,
-        timelineScale,
         togglePlayPause,
         toggleSkipInactive,
         ...value,

+ 33 - 25
static/app/components/replays/timeAndScrubberGrid.tsx

@@ -11,6 +11,9 @@ import {IconAdd, IconSubtract} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import formatReplayDuration from 'sentry/utils/duration/formatReplayDuration';
+import useTimelineScale, {
+  TimelineScaleContextProvider,
+} from 'sentry/utils/replays/hooks/useTimelineScale';
 
 type TimeAndScrubberGridProps = {
   isCompact?: boolean;
@@ -18,12 +21,13 @@ type TimeAndScrubberGridProps = {
 };
 
 function TimelineSizeBar() {
-  const {replay, timelineScale, setTimelineScale} = useReplayContext();
+  const {replay} = useReplayContext();
+  const [timelineScale, setTimelineScale] = useTimelineScale();
   const durationMs = replay?.getDurationMs();
   const maxScale = durationMs ? Math.ceil(durationMs / 60000) : 10;
 
   return (
-    <ButtonBar>
+    <ButtonBar gap={0.5}>
       <Button
         size="xs"
         title={t('Zoom out')}
@@ -33,10 +37,10 @@ function TimelineSizeBar() {
         aria-label={t('Zoom out')}
         disabled={timelineScale === 1}
       />
-      <span style={{padding: `0 ${space(0.5)}`}}>
+      <Numeric>
         {timelineScale}
-        {t('x')}
-      </span>
+        {'\u00D7'}
+      </Numeric>
       <Button
         size="xs"
         title={t('Zoom in')}
@@ -50,7 +54,7 @@ function TimelineSizeBar() {
   );
 }
 
-function TimeAndScrubberGrid({
+export default function TimeAndScrubberGrid({
   isCompact = false,
   showZoom = false,
 }: TimeAndScrubberGridProps) {
@@ -60,21 +64,25 @@ function TimeAndScrubberGrid({
   const mouseTrackingProps = useScrubberMouseTracking({elem});
 
   return (
-    <Grid id="replay-timeline-player" isCompact={isCompact}>
-      <Time style={{gridArea: 'currentTime'}}>{formatReplayDuration(currentTime)}</Time>
-      <div style={{gridArea: 'timeline'}}>
-        <ReplayTimeline />
-      </div>
-      <div style={{gridArea: 'timelineSize', fontVariantNumeric: 'tabular-nums'}}>
-        {showZoom ? <TimelineSizeBar /> : null}
-      </div>
-      <StyledScrubber style={{gridArea: 'scrubber'}} ref={elem} {...mouseTrackingProps}>
-        <PlayerScrubber showZoomIndicators={showZoom} />
-      </StyledScrubber>
-      <Time style={{gridArea: 'duration'}}>
-        {durationMs ? formatReplayDuration(durationMs) : '--:--'}
-      </Time>
-    </Grid>
+    <TimelineScaleContextProvider>
+      <Grid id="replay-timeline-player" isCompact={isCompact}>
+        <Numeric style={{gridArea: 'currentTime', paddingInline: space(1.5)}}>
+          {formatReplayDuration(currentTime)}
+        </Numeric>
+        <div style={{gridArea: 'timeline'}}>
+          <ReplayTimeline />
+        </div>
+        <div style={{gridArea: 'timelineSize', fontVariantNumeric: 'tabular-nums'}}>
+          {showZoom ? <TimelineSizeBar /> : null}
+        </div>
+        <StyledScrubber style={{gridArea: 'scrubber'}} ref={elem} {...mouseTrackingProps}>
+          <PlayerScrubber showZoomIndicators={showZoom} />
+        </StyledScrubber>
+        <Numeric style={{gridArea: 'duration', paddingInline: space(1.5)}}>
+          {durationMs ? formatReplayDuration(durationMs) : '--:--'}
+        </Numeric>
+      </Grid>
+    </TimelineScaleContextProvider>
   );
 }
 
@@ -103,9 +111,9 @@ const StyledScrubber = styled('div')`
   align-items: center;
 `;
 
-const Time = styled('span')`
+const Numeric = styled('span')`
+  color: ${p => p.theme.gray300};
+  font-size: ${p => p.theme.fontSizeSmall};
   font-variant-numeric: tabular-nums;
-  padding: 0 ${space(1.5)};
+  font-weight: ${p => p.theme.fontWeightBold};
 `;
-
-export default TimeAndScrubberGrid;

+ 16 - 0
static/app/utils/replays/hooks/useTimelineScale.tsx

@@ -0,0 +1,16 @@
+import {createContext, useContext, useState} from 'react';
+
+const context = createContext<[scale: number, setScale: (scale: number) => void]>([
+  1,
+  (_scale: number) => {},
+]);
+
+export function TimelineScaleContextProvider({children}: {children: React.ReactNode}) {
+  const state = useState(1);
+
+  return <context.Provider value={state}>{children}</context.Provider>;
+}
+
+export default function useTimelineScale() {
+  return useContext(context);
+}