mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
fix: Update session player (#395)
* Replace react-slider with mantine's slider + custom markers for better performance * Fix bug when opening a session replay with a ?ts param shows the first frame instead of correct frame at `ts` * abort loading events when navigating off the page before https://github.com/hyperdxio/hyperdx/assets/20255948/195ce791-2d31-4ae4-9700-0ff52f021171 after https://github.com/hyperdxio/hyperdx/assets/20255948/8ec31ff4-c3c1-4c0d-9f04-29c123e9444f
This commit is contained in:
parent
9c4ef4320c
commit
6d99e3be9c
11 changed files with 248 additions and 275 deletions
5
.changeset/flat-cups-invite.md
Normal file
5
.changeset/flat-cups-invite.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hyperdx/app': patch
|
||||
---
|
||||
|
||||
New performant session replay playbar component
|
||||
|
|
@ -72,7 +72,6 @@
|
|||
"react-query": "^3.39.1",
|
||||
"react-reflex": "^4.0.9",
|
||||
"react-select": "^5.7.0",
|
||||
"react-slider": "^2.0.1",
|
||||
"react-sliding-pane": "^7.3.0",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
|
|
@ -109,7 +108,6 @@
|
|||
"@types/react-datepicker": "^4.10.0",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-grid-layout": "^1.3.2",
|
||||
"@types/react-slider": "^1.3.1",
|
||||
"@types/react-syntax-highlighter": "^13.5.2",
|
||||
"@types/react-table": "^7.7.14",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
|
|
|
|||
|
|
@ -476,7 +476,7 @@ function SearchInput({
|
|||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={e => onChange(e.currentTarget.value)}
|
||||
leftSection={<i className="bi bi-search fs-8 ps-1" text-slate-400 />}
|
||||
leftSection={<i className="bi bi-search fs-8 ps-1 text-slate-400" />}
|
||||
onKeyDown={handleKeyDown}
|
||||
rightSection={
|
||||
value ? (
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export default function DOMPlayer({
|
|||
|
||||
let currentRrwebEvent = '';
|
||||
|
||||
const { isFetching: isSearchResultsFetching } = useSearchEventStream(
|
||||
const { isFetching: isSearchResultsFetching, abort } = useSearchEventStream(
|
||||
{
|
||||
apiUrlPath: `/sessions/${sessionId}/rrweb`,
|
||||
q: '',
|
||||
|
|
@ -219,21 +219,7 @@ export default function DOMPlayer({
|
|||
replayer.current?.setConfig({ speed: playerSpeed, skipInactive });
|
||||
}
|
||||
}
|
||||
}, [playerState, playerSpeed, skipInactive]);
|
||||
|
||||
// Set player to the correct time based on focus
|
||||
useEffect(() => {
|
||||
if (focus?.setBy !== 'player' && replayer.current != null) {
|
||||
pause(
|
||||
focus?.ts == null
|
||||
? 0
|
||||
: focus?.ts - replayer.current.getMetaData().startTime,
|
||||
);
|
||||
if (playerState === 'playing') {
|
||||
play();
|
||||
}
|
||||
}
|
||||
}, [focus, pause, setPlayerState, playerState, play]);
|
||||
}, [playerState, playerSpeed, skipInactive, pause, play]);
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
if (wrapper.current == null || playerContainer.current == null) {
|
||||
|
|
@ -262,6 +248,7 @@ export default function DOMPlayer({
|
|||
handleResize();
|
||||
}, [resizeKey, handleResize]);
|
||||
|
||||
const [isReplayerInitialized, setIsReplayerInitialized] = useState(false);
|
||||
// Set up player
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
|
@ -281,6 +268,7 @@ export default function DOMPlayer({
|
|||
showWarning: debug,
|
||||
skipInactive: true,
|
||||
});
|
||||
setIsReplayerInitialized(true);
|
||||
|
||||
if (debug) {
|
||||
// @ts-ignore
|
||||
|
|
@ -334,19 +322,51 @@ export default function DOMPlayer({
|
|||
debug,
|
||||
]);
|
||||
|
||||
// Set player to the correct time based on focus
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isInitialEventsLoaded ||
|
||||
!isReplayerInitialized ||
|
||||
lastEventTsLoaded < (focus?.ts ? focus.ts + 1000 : Infinity)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (focus?.setBy !== 'player' && replayer.current != null) {
|
||||
pause(
|
||||
focus?.ts == null
|
||||
? 0
|
||||
: focus?.ts - replayer.current.getMetaData().startTime,
|
||||
);
|
||||
handleResize();
|
||||
if (playerState === 'playing') {
|
||||
play();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
focus,
|
||||
pause,
|
||||
setPlayerState,
|
||||
playerState,
|
||||
play,
|
||||
isInitialEventsLoaded,
|
||||
isReplayerInitialized,
|
||||
handleResize,
|
||||
lastEventTsLoaded,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (replayer.current != null) {
|
||||
replayer.current?.destroy();
|
||||
replayer.current = null;
|
||||
}
|
||||
abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isLoading = isInitialEventsLoaded === false && isSearchResultsFetching;
|
||||
// TODO: Handle when ts is set to a value that's outside of this session
|
||||
const isBuffering =
|
||||
playerState === 'playing' &&
|
||||
isReplayFullyLoaded === false &&
|
||||
(replayer.current?.getMetaData()?.endTime ?? 0) < (focus?.ts ?? 0);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import ReactSlider from 'react-slider';
|
||||
import { Group } from '@mantine/core';
|
||||
|
||||
import Checkbox from './Checkbox';
|
||||
import type { PlaybarMarker } from './PlaybarSlider';
|
||||
import { PlaybarSlider } from './PlaybarSlider';
|
||||
import { useSessionEvents } from './sessionUtils';
|
||||
import { getShortUrl, truncateText, useLocalStorage } from './utils';
|
||||
import { getShortUrl, useLocalStorage } from './utils';
|
||||
|
||||
import 'react-bootstrap-range-slider/dist/react-bootstrap-range-slider.css';
|
||||
|
||||
|
|
@ -38,102 +38,6 @@ function formatTs({
|
|||
}
|
||||
}
|
||||
|
||||
function Track({
|
||||
props,
|
||||
state,
|
||||
minSliderVal,
|
||||
maxSliderVal,
|
||||
showRelativeTime,
|
||||
}: {
|
||||
props: any;
|
||||
state: any;
|
||||
minSliderVal: number;
|
||||
maxSliderVal: number;
|
||||
showRelativeTime: boolean;
|
||||
}) {
|
||||
const thumbWidth = 18;
|
||||
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const tracker = useRef<HTMLDivElement>(null);
|
||||
const [track0MouseXPerc, setTrack0MouseXPerc] = useState<
|
||||
undefined | number
|
||||
>();
|
||||
const [mouseHovered, setMouseHovered] = useState(false);
|
||||
const [mouseTs, setMouseTs] = useState<number | undefined>();
|
||||
|
||||
const debouncedMouseMove = useMemo(
|
||||
() =>
|
||||
throttle(e => {
|
||||
const rect = container.current?.getBoundingClientRect();
|
||||
if (rect == null) return;
|
||||
|
||||
// https://github.com/zillow/react-slider/blob/master/src/components/ReactSlider/ReactSlider.jsx#L749
|
||||
// For some reason we need to subtract half of thumb width here to match
|
||||
// the react slider logic
|
||||
const x = e.clientX - rect.left - thumbWidth / 2;
|
||||
// Subtract by thumb width as the thumb width is added to each track
|
||||
const xPerc = x / (rect.width - thumbWidth);
|
||||
|
||||
const segmentTimespan =
|
||||
state.index === 0
|
||||
? state.value - minSliderVal
|
||||
: maxSliderVal - state.value;
|
||||
|
||||
const newMouseTs =
|
||||
segmentTimespan * xPerc +
|
||||
(state.index === 0 ? minSliderVal : state.value);
|
||||
|
||||
setMouseTs(newMouseTs);
|
||||
setTrack0MouseXPerc(xPerc);
|
||||
}, 100),
|
||||
[minSliderVal, state.value, state.index, maxSliderVal],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={
|
||||
state.index === 0
|
||||
? 'bg-success rounded-pill rounded-end top-0 bottom-0'
|
||||
: 'bg-grey rounded-pill rounded-start top-0 bottom-0'
|
||||
}
|
||||
key={`track-${state.index}`}
|
||||
onMouseMove={debouncedMouseMove}
|
||||
ref={container}
|
||||
onMouseEnter={() => setMouseHovered(true)}
|
||||
onMouseLeave={() => setMouseHovered(false)}
|
||||
>
|
||||
<div
|
||||
ref={tracker}
|
||||
style={{
|
||||
width: `${(track0MouseXPerc ?? 0) * 100}%`,
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
top: '-15px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="rounded bg-black text-center text-nowrap"
|
||||
style={{
|
||||
display: mouseHovered ? 'block' : 'none',
|
||||
position: 'absolute',
|
||||
transform: 'translateX(50%)',
|
||||
padding: 5,
|
||||
right: 0,
|
||||
top: -20,
|
||||
}}
|
||||
>
|
||||
{formatTs({
|
||||
ts: mouseTs,
|
||||
minTs: minSliderVal,
|
||||
showRelativeTime,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Playbar({
|
||||
playerState,
|
||||
setPlayerState,
|
||||
|
|
@ -173,47 +77,50 @@ export default function Playbar({
|
|||
|
||||
const { events } = useSessionEvents({ config: eventsConfig });
|
||||
|
||||
const markers = useMemo(() => {
|
||||
return (
|
||||
events?.map(event => {
|
||||
const spanName = event['span_name'];
|
||||
const locationHref = event['location.href'];
|
||||
const shortLocationHref = getShortUrl(locationHref);
|
||||
const markers = useMemo<PlaybarMarker[]>(() => {
|
||||
return uniqBy(
|
||||
events
|
||||
?.filter(
|
||||
({ startOffset }) => startOffset >= minTs && startOffset <= maxTs,
|
||||
)
|
||||
.map(event => {
|
||||
const spanName = event['span_name'];
|
||||
const locationHref = event['location.href'];
|
||||
const shortLocationHref = getShortUrl(locationHref);
|
||||
|
||||
const errorMessage = event['error.message'];
|
||||
const errorMessage = event['error.message'];
|
||||
|
||||
const url = event['http.url'];
|
||||
const statusCode = event['http.status_code'];
|
||||
const method = event['http.method'];
|
||||
const shortUrl = getShortUrl(url);
|
||||
const url = event['http.url'];
|
||||
const statusCode = event['http.status_code'];
|
||||
const method = event['http.method'];
|
||||
const shortUrl = getShortUrl(url);
|
||||
|
||||
const isNavigation =
|
||||
spanName === 'routeChange' || spanName === 'documentLoad';
|
||||
const isNavigation =
|
||||
spanName === 'routeChange' || spanName === 'documentLoad';
|
||||
|
||||
const isError = event.severity_text === 'error' || statusCode >= 399;
|
||||
const isError = event.severity_text === 'error' || statusCode >= 399;
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
ts: event.startOffset,
|
||||
description: isNavigation
|
||||
? `Navigated to ${shortLocationHref}`
|
||||
: url.length > 0
|
||||
? `${statusCode} ${method}${url.length > 0 ? ` ${shortUrl}` : ''}`
|
||||
: errorMessage != null && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: spanName === 'intercom.onShow'
|
||||
? 'Intercom Chat Opened'
|
||||
: event.body,
|
||||
className: isError ? 'bg-danger' : 'bg-primary',
|
||||
};
|
||||
}) ?? []
|
||||
return {
|
||||
id: event.id,
|
||||
ts: event.startOffset,
|
||||
percentage: Math.round(
|
||||
((event.startOffset - minTs) / (maxTs - minTs)) * 100,
|
||||
),
|
||||
description: isNavigation
|
||||
? `Navigated to ${shortLocationHref}`
|
||||
: url.length > 0
|
||||
? `${statusCode} ${method}${url.length > 0 ? ` ${shortUrl}` : ''}`
|
||||
: errorMessage != null && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: spanName === 'intercom.onShow'
|
||||
? 'Intercom Chat Opened'
|
||||
: event.body,
|
||||
isError,
|
||||
};
|
||||
}) ?? [],
|
||||
'percentage',
|
||||
);
|
||||
}, [events]);
|
||||
|
||||
const marks = useMemo(
|
||||
() => Array.from(new Set(markers.map(m => m.ts).filter(ts => ts >= minTs))),
|
||||
[markers, minTs],
|
||||
);
|
||||
}, [events, maxTs, minTs]);
|
||||
|
||||
const [showRelativeTime, setShowRelativeTime] = useLocalStorage(
|
||||
'hdx-session-subpanel-show-relative-time',
|
||||
|
|
@ -280,71 +187,17 @@ export default function Playbar({
|
|||
>
|
||||
{formatTs({ ts: focus?.ts, minTs, showRelativeTime })}
|
||||
</div>
|
||||
<div className="PlaybarSliderParent w-100 d-flex align-self-stretch align-items-center me-3">
|
||||
<ReactSlider
|
||||
className="PlaybarSlider w-100"
|
||||
thumbClassName="thumb"
|
||||
value={focus?.ts ?? minSliderVal}
|
||||
<div className="w-100 d-flex align-self-stretch align-items-center me-3">
|
||||
<PlaybarSlider
|
||||
markers={markers}
|
||||
min={minSliderVal}
|
||||
max={maxSliderVal}
|
||||
step={1000}
|
||||
marks={marks}
|
||||
renderMark={props => {
|
||||
const mark = markers.find(marker => marker.ts === props.key);
|
||||
const description = truncateText(
|
||||
mark?.description ?? '',
|
||||
240,
|
||||
'...',
|
||||
/\n/,
|
||||
);
|
||||
return (
|
||||
<OverlayTrigger
|
||||
key={`${props.key}`}
|
||||
overlay={<Tooltip id={`tooltip`}>{description}</Tooltip>}
|
||||
>
|
||||
<div
|
||||
{...props}
|
||||
className={`${mark?.className ?? ''} rounded-circle mark`}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
value={focus?.ts}
|
||||
onChange={ts => {
|
||||
setFocus({ ts, setBy: 'slider' });
|
||||
}}
|
||||
renderThumb={(props, state) => (
|
||||
<OverlayTrigger
|
||||
key="thumb"
|
||||
overlay={
|
||||
<Tooltip id={`tooltip`} className="mono fs-7 text-nowrap">
|
||||
{(() => {
|
||||
const value = Math.max(state.value - minTs, 0);
|
||||
const minutes = Math.floor(value / 1000 / 60);
|
||||
const seconds = Math.floor((value / 1000) % 60);
|
||||
return `${minutes}m:${seconds < 10 ? '0' : ''}${seconds}s`;
|
||||
})()}{' '}
|
||||
at{' '}
|
||||
{(() => {
|
||||
return format(new Date(state.value), 'hh:mm:ss a');
|
||||
})()}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div
|
||||
{...props}
|
||||
className="bg-success cursor-grab rounded-circle shadow thumb"
|
||||
key="thumb"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
renderTrack={(props, state) => (
|
||||
<Track
|
||||
key={state.index}
|
||||
props={props}
|
||||
state={state}
|
||||
minSliderVal={minSliderVal}
|
||||
maxSliderVal={maxSliderVal}
|
||||
showRelativeTime={showRelativeTime}
|
||||
/>
|
||||
)}
|
||||
onChange={value => setFocus({ ts: value as number, setBy: 'slider' })}
|
||||
setPlayerState={setPlayerState}
|
||||
playerState={playerState}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
|
|
|
|||
98
packages/app/src/PlaybarSlider.tsx
Normal file
98
packages/app/src/PlaybarSlider.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import * as React from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { Slider, Tooltip } from '@mantine/core';
|
||||
|
||||
import { truncateText } from './utils';
|
||||
|
||||
import styles from '../styles/PlaybarSlider.module.scss';
|
||||
|
||||
export type PlaybarMarker = {
|
||||
id: string;
|
||||
ts: number;
|
||||
description: string;
|
||||
isError: boolean;
|
||||
};
|
||||
|
||||
type PlaybarSliderProps = {
|
||||
value?: number;
|
||||
min: number;
|
||||
max: number;
|
||||
markers?: PlaybarMarker[];
|
||||
playerState: 'playing' | 'paused';
|
||||
onChange: (value: number) => void;
|
||||
setPlayerState: (playerState: 'playing' | 'paused') => void;
|
||||
};
|
||||
|
||||
export const PlaybarSlider = ({
|
||||
min,
|
||||
max,
|
||||
value,
|
||||
markers,
|
||||
playerState,
|
||||
onChange,
|
||||
setPlayerState,
|
||||
}: PlaybarSliderProps) => {
|
||||
const valueLabelFormat = React.useCallback(
|
||||
(ts: number) => {
|
||||
const value = Math.max(ts - min, 0);
|
||||
const minutes = Math.floor(value / 1000 / 60);
|
||||
const seconds = Math.floor((value / 1000) % 60);
|
||||
const timestamp = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
||||
const time = format(new Date(ts), 'hh:mm:ss a');
|
||||
return `${timestamp} at ${time}`;
|
||||
},
|
||||
[min],
|
||||
);
|
||||
|
||||
const markersContent = React.useMemo(
|
||||
() =>
|
||||
markers?.map(mark => (
|
||||
<Tooltip
|
||||
color={mark.isError ? 'red' : 'gray'}
|
||||
key={mark.id}
|
||||
label={truncateText(mark?.description ?? '', 240, '...', /\n/)}
|
||||
position="top"
|
||||
withArrow
|
||||
>
|
||||
<div
|
||||
className={styles.markerDot}
|
||||
style={{
|
||||
backgroundColor: mark.isError
|
||||
? 'var(--mantine-color-red-6)'
|
||||
: 'var(--mantine-color-gray-6)',
|
||||
left: `${((mark.ts - min) / (max - min)) * 100}%`,
|
||||
}}
|
||||
onClick={() => onChange(mark.ts)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)),
|
||||
[markers, max, min, onChange],
|
||||
);
|
||||
|
||||
const [prevPlayerState, setPrevPlayerState] = React.useState(playerState);
|
||||
const handleMouseDown = React.useCallback(() => {
|
||||
setPrevPlayerState(playerState);
|
||||
setPlayerState('paused');
|
||||
}, [playerState, setPlayerState]);
|
||||
const handleMouseUp = React.useCallback(() => {
|
||||
setPlayerState(prevPlayerState);
|
||||
}, [prevPlayerState, setPlayerState]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.markers}>{markersContent}</div>
|
||||
<Slider
|
||||
color="gray.5"
|
||||
size="sm"
|
||||
min={min}
|
||||
max={max}
|
||||
value={value || min}
|
||||
step={1000}
|
||||
label={valueLabelFormat}
|
||||
onChange={onChange}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -326,11 +326,13 @@ export default function SessionSubpanel({
|
|||
},
|
||||
);
|
||||
const prevTsQuery = usePrevious(tsQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevTsQuery == null && tsQuery != null) {
|
||||
_setFocus({ ts: tsQuery, setBy: 'url' });
|
||||
}
|
||||
}, [prevTsQuery, tsQuery]);
|
||||
|
||||
const debouncedSetTsQuery = useRef(
|
||||
throttle(async (ts: number) => {
|
||||
setTsQuery(ts, 'replaceIn');
|
||||
|
|
|
|||
|
|
@ -292,12 +292,17 @@ function useSearchEventStream(
|
|||
[fetchResults, results.data.length, hasNextPage],
|
||||
);
|
||||
|
||||
const abort = useCallback(() => {
|
||||
lastAbortController.current?.abort();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
hasNextPage,
|
||||
isFetching,
|
||||
results: results.data,
|
||||
resultsKey: results.key,
|
||||
fetchNextPage,
|
||||
abort,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
36
packages/app/styles/PlaybarSlider.module.scss
Normal file
36
packages/app/styles/PlaybarSlider.module.scss
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
.wrapper {
|
||||
width: 100%;
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.markers {
|
||||
width: auto !important;
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
|
||||
&:hover {
|
||||
.markerDot {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markerDot {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
margin-left: -1px;
|
||||
width: 3px;
|
||||
height: 8px;
|
||||
background-color: #333;
|
||||
border-radius: 1px;
|
||||
&:hover {
|
||||
opacity: 1 !important;
|
||||
z-index: 20;
|
||||
}
|
||||
}
|
||||
|
|
@ -371,6 +371,19 @@ body {
|
|||
|
||||
/* Custom Components */
|
||||
|
||||
.player-skipping-overlay {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: rgba(50, 50, 50, 0.4);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.player-wrapper {
|
||||
position: relative;
|
||||
|
||||
|
|
@ -427,49 +440,6 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.PlaybarSliderParent {
|
||||
&:hover {
|
||||
.PlaybarSlider {
|
||||
height: 12px;
|
||||
|
||||
.thumb {
|
||||
margin-top: -3px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.mark {
|
||||
margin-top: 0px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.PlaybarSlider {
|
||||
transition: height 0.2s ease-in-out;
|
||||
height: 6px;
|
||||
|
||||
.thumb {
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
margin-top: -5px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.mark {
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: -1px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Just for the price slider
|
||||
.PricingSlider {
|
||||
height: 25px;
|
||||
|
|
|
|||
14
yarn.lock
14
yarn.lock
|
|
@ -4406,13 +4406,6 @@
|
|||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-slider@^1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-slider/-/react-slider-1.3.1.tgz#a3816989eb4fc172e7df316930f242fec90690fc"
|
||||
integrity sha512-4X2yK7RyCIy643YCFL+bc6XNmcnBtt8n88uuyihvcn5G7Lut23eNQU3q3KmwF7MWIfKfsW5NxCjw0SeDZRtgaA==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-syntax-highlighter@^13.5.2":
|
||||
version "13.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-13.5.2.tgz#357cc03581dc434c57c3b31f70e0eecdbf7b3ab0"
|
||||
|
|
@ -12188,13 +12181,6 @@ react-select@^5.7.0:
|
|||
react-transition-group "^4.3.0"
|
||||
use-isomorphic-layout-effect "^1.1.2"
|
||||
|
||||
react-slider@^2.0.1:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-2.0.4.tgz#21c656ffabc3bb4481cf6b49e6d647baeda83572"
|
||||
integrity sha512-sWwQD01n6v+MbeLCYthJGZPc0kzOyhQHyd0bSo0edg+IAxTVQmj3Oy4SBK65eX6gNwS9meUn6Z5sIBUVmwAd9g==
|
||||
dependencies:
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-sliding-pane@^7.3.0:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-sliding-pane/-/react-sliding-pane-7.3.0.tgz#a6a03b90db216e7ec6f746c7e649d19ba03ff4e0"
|
||||
|
|
|
|||
Loading…
Reference in a new issue