diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx index f2e5adb29..88cf515c1 100644 --- a/web/src/components/timeline/EventReviewTimeline.tsx +++ b/web/src/components/timeline/EventReviewTimeline.tsx @@ -10,6 +10,7 @@ import { import EventSegment from "./EventSegment"; import { useEventUtils } from "@/hooks/use-event-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; +import ReviewTimeline from "./ReviewTimeline"; export type EventReviewTimelineProps = { segmentDuration: number; @@ -48,7 +49,6 @@ export function EventReviewTimeline({ const scrollTimeRef = useRef(null); const timelineRef = useRef(null); const handlebarTimeRef = useRef(null); - const observer = useRef(null); const timelineDuration = useMemo( () => timelineStart - timelineEnd, [timelineEnd, timelineStart], @@ -77,28 +77,6 @@ export function EventReviewTimeline({ handlebarTimeRef, }); - function handleResize() { - // TODO: handle screen resize for mobile - // eslint-disable-next-line no-empty - if (timelineRef.current && contentRef.current) { - } - } - - useEffect(() => { - if (contentRef.current) { - const content = contentRef.current; - observer.current = new ResizeObserver(() => { - handleResize(); - }); - observer.current.observe(content); - return () => { - observer.current?.unobserve(content); - }; - } - // should only be calculated at beginning - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - // Generate segments for the timeline const generateSegments = useCallback(() => { const segmentCount = timelineDuration / segmentDuration; @@ -158,46 +136,19 @@ export function EventReviewTimeline({ }, [isDragging, onHandlebarDraggingChange]); return ( -
-
{segments}
- {showHandlebar && ( -
-
-
-
-
-
-
-
-
-
- )} -
+ {segments} + ); } diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx index eed161804..25385fa8d 100644 --- a/web/src/components/timeline/EventSegment.tsx +++ b/web/src/components/timeline/EventSegment.tsx @@ -1,6 +1,6 @@ import { useApiHost } from "@/api"; import { useEventUtils } from "@/hooks/use-event-utils"; -import { useSegmentUtils } from "@/hooks/use-segment-utils"; +import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; import React, { RefObject, @@ -9,7 +9,6 @@ import React, { useMemo, useRef, } from "react"; -import { isDesktop } from "react-device-detect"; import { HoverCard, HoverCardContent, @@ -17,6 +16,7 @@ import { } from "../ui/hover-card"; import { HoverCardPortal } from "@radix-ui/react-hover-card"; import scrollIntoView from "scroll-into-view-if-needed"; +import { MinimapBounds, Tick, Timestamp } from "./segment-metadata"; type EventSegmentProps = { events: ReviewSegment[]; @@ -30,105 +30,6 @@ type EventSegmentProps = { contentRef: RefObject; }; -type MinimapSegmentProps = { - isFirstSegmentInMinimap: boolean; - isLastSegmentInMinimap: boolean; - alignedMinimapStartTime: number; - alignedMinimapEndTime: number; - firstMinimapSegmentRef: React.MutableRefObject; -}; - -type TickSegmentProps = { - timestamp: Date; - timestampSpread: number; -}; - -type TimestampSegmentProps = { - isFirstSegmentInMinimap: boolean; - isLastSegmentInMinimap: boolean; - timestamp: Date; - timestampSpread: number; - segmentKey: number; -}; - -function MinimapBounds({ - isFirstSegmentInMinimap, - isLastSegmentInMinimap, - alignedMinimapStartTime, - alignedMinimapEndTime, - firstMinimapSegmentRef, -}: MinimapSegmentProps) { - return ( - <> - {isFirstSegmentInMinimap && ( -
- {new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - ...(isDesktop && { month: "short", day: "2-digit" }), - })} -
- )} - - {isLastSegmentInMinimap && ( -
- {new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - ...(isDesktop && { month: "short", day: "2-digit" }), - })} -
- )} - - ); -} - -function Tick({ timestamp, timestampSpread }: TickSegmentProps) { - return ( -
-
-
-
-
- ); -} - -function Timestamp({ - isFirstSegmentInMinimap, - isLastSegmentInMinimap, - timestamp, - timestampSpread, - segmentKey, -}: TimestampSegmentProps) { - return ( -
- {!isFirstSegmentInMinimap && !isLastSegmentInMinimap && ( -
- {timestamp.getMinutes() % timestampSpread === 0 && - timestamp.getSeconds() === 0 && - timestamp.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} -
- )} -
- ); -} - export function EventSegment({ events, segmentTime, @@ -147,7 +48,7 @@ export function EventSegment({ shouldShowRoundedCorners, getEventStart, getEventThumbnail, - } = useSegmentUtils(segmentDuration, events, severityType); + } = useEventSegmentUtils(segmentDuration, events, severityType); const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils( events, @@ -294,11 +195,7 @@ export function EventSegment({ }, [startTimestamp]); return ( -
+
- {severity.map((severityValue, index) => ( + {severity.map((severityValue: number, index: number) => ( {severityValue === displaySeverityType && ( diff --git a/web/src/components/timeline/MotionReviewTimeline.tsx b/web/src/components/timeline/MotionReviewTimeline.tsx new file mode 100644 index 000000000..4f1da2962 --- /dev/null +++ b/web/src/components/timeline/MotionReviewTimeline.tsx @@ -0,0 +1,157 @@ +import useDraggableHandler from "@/hooks/use-handle-dragging"; +import { + useEffect, + useCallback, + useMemo, + useRef, + useState, + RefObject, +} from "react"; +import MotionSegment from "./MotionSegment"; +import { useEventUtils } from "@/hooks/use-event-utils"; +import { ReviewSegment, ReviewSeverity } from "@/types/review"; +import ReviewTimeline from "./ReviewTimeline"; +import { MockMotionData } from "@/pages/UIPlayground"; + +export type MotionReviewTimelineProps = { + segmentDuration: number; + timestampSpread: number; + timelineStart: number; + timelineEnd: number; + showHandlebar?: boolean; + handlebarTime?: number; + setHandlebarTime?: React.Dispatch>; + showMinimap?: boolean; + minimapStartTime?: number; + minimapEndTime?: number; + events: ReviewSegment[]; + motion_events: MockMotionData[]; + severityType: ReviewSeverity; + contentRef: RefObject; + onHandlebarDraggingChange?: (isDragging: boolean) => void; +}; + +export function MotionReviewTimeline({ + segmentDuration, + timestampSpread, + timelineStart, + timelineEnd, + showHandlebar = false, + handlebarTime, + setHandlebarTime, + showMinimap = false, + minimapStartTime, + minimapEndTime, + events, + motion_events, + contentRef, + onHandlebarDraggingChange, +}: MotionReviewTimelineProps) { + const [isDragging, setIsDragging] = useState(false); + const scrollTimeRef = useRef(null); + const timelineRef = useRef(null); + const handlebarTimeRef = useRef(null); + const timelineDuration = useMemo( + () => timelineStart - timelineEnd, + [timelineEnd, timelineStart], + ); + + const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils( + events, + segmentDuration, + ); + + const { handleMouseDown, handleMouseUp, handleMouseMove } = + useDraggableHandler({ + contentRef, + timelineRef, + scrollTimeRef, + alignStartDateToTimeline, + alignEndDateToTimeline, + segmentDuration, + showHandlebar, + handlebarTime, + setHandlebarTime, + timelineDuration, + timelineStart, + isDragging, + setIsDragging, + handlebarTimeRef, + }); + + // Generate segments for the timeline + const generateSegments = useCallback(() => { + const segmentCount = timelineDuration / segmentDuration; + const segmentAlignedTime = alignStartDateToTimeline(timelineStart); + + return Array.from({ length: segmentCount }, (_, index) => { + const segmentTime = segmentAlignedTime - index * segmentDuration; + + return ( + + ); + }); + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + segmentDuration, + timestampSpread, + timelineStart, + timelineDuration, + showMinimap, + minimapStartTime, + minimapEndTime, + events, + ]); + + const segments = useMemo( + () => generateSegments(), + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + segmentDuration, + timestampSpread, + timelineStart, + timelineDuration, + showMinimap, + minimapStartTime, + minimapEndTime, + events, + ], + ); + + useEffect(() => { + if (onHandlebarDraggingChange) { + onHandlebarDraggingChange(isDragging); + } + }, [isDragging, onHandlebarDraggingChange]); + + return ( + + {segments} + + ); +} + +export default MotionReviewTimeline; diff --git a/web/src/components/timeline/MotionSegment.tsx b/web/src/components/timeline/MotionSegment.tsx new file mode 100644 index 000000000..b898611ca --- /dev/null +++ b/web/src/components/timeline/MotionSegment.tsx @@ -0,0 +1,307 @@ +import { useEventUtils } from "@/hooks/use-event-utils"; +import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils"; +import { ReviewSegment } from "@/types/review"; +import React, { + RefObject, + useCallback, + useEffect, + useMemo, + useRef, +} from "react"; +import scrollIntoView from "scroll-into-view-if-needed"; +import { MinimapBounds, Tick, Timestamp } from "./segment-metadata"; +import { MockMotionData } from "@/pages/UIPlayground"; +import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils"; +import { isMobile } from "react-device-detect"; + +type MotionSegmentProps = { + events: ReviewSegment[]; + motion_events: MockMotionData[]; + segmentTime: number; + segmentDuration: number; + timestampSpread: number; + showMinimap: boolean; + minimapStartTime?: number; + minimapEndTime?: number; + contentRef: RefObject; +}; + +export function MotionSegment({ + events, + motion_events, + segmentTime, + segmentDuration, + timestampSpread, + showMinimap, + minimapStartTime, + minimapEndTime, + contentRef, +}: MotionSegmentProps) { + const severityType = "all"; + const { + getSeverity, + getReviewed, + displaySeverityType, + shouldShowRoundedCorners, + getEventStart, + } = useEventSegmentUtils(segmentDuration, events, severityType); + + const { + getMotionSegmentValue, + getAudioSegmentValue, + interpolateMotionAudioData, + } = useMotionSegmentUtils(segmentDuration, motion_events); + + const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils( + events, + segmentDuration, + ); + + const severity = useMemo( + () => getSeverity(segmentTime, displaySeverityType), + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + [getSeverity, segmentTime], + ); + + const reviewed = useMemo( + () => getReviewed(segmentTime), + [getReviewed, segmentTime], + ); + + const { roundTopSecondary, roundBottomSecondary } = useMemo( + () => shouldShowRoundedCorners(segmentTime), + [shouldShowRoundedCorners, segmentTime], + ); + + const startTimestamp = useMemo(() => { + const eventStart = getEventStart(segmentTime); + if (eventStart) { + return alignStartDateToTimeline(eventStart); + } + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getEventStart, segmentTime]); + + const timestamp = useMemo(() => new Date(segmentTime * 1000), [segmentTime]); + const segmentKey = useMemo(() => segmentTime, [segmentTime]); + + const maxSegmentWidth = useMemo(() => { + return isMobile ? 15 : 25; + }, []); + + const alignedMinimapStartTime = useMemo( + () => alignStartDateToTimeline(minimapStartTime ?? 0), + [minimapStartTime, alignStartDateToTimeline], + ); + const alignedMinimapEndTime = useMemo( + () => alignEndDateToTimeline(minimapEndTime ?? 0), + [minimapEndTime, alignEndDateToTimeline], + ); + + const isInMinimapRange = useMemo(() => { + return ( + showMinimap && + segmentTime >= alignedMinimapStartTime && + segmentTime < alignedMinimapEndTime + ); + }, [ + showMinimap, + alignedMinimapStartTime, + alignedMinimapEndTime, + segmentTime, + ]); + + const isFirstSegmentInMinimap = useMemo(() => { + return showMinimap && segmentTime === alignedMinimapStartTime; + }, [showMinimap, segmentTime, alignedMinimapStartTime]); + + const isLastSegmentInMinimap = useMemo(() => { + return showMinimap && segmentTime === alignedMinimapEndTime; + }, [showMinimap, segmentTime, alignedMinimapEndTime]); + + const firstMinimapSegmentRef = useRef(null); + + useEffect(() => { + // Check if the first segment is out of view + const firstSegment = firstMinimapSegmentRef.current; + if (firstSegment && showMinimap && isFirstSegmentInMinimap) { + scrollIntoView(firstSegment, { + scrollMode: "if-needed", + behavior: "smooth", + }); + } + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showMinimap, isFirstSegmentInMinimap, events, segmentDuration]); + + const segmentClasses = `h-2 relative w-full ${ + showMinimap + ? isInMinimapRange + ? "bg-secondary-highlight" + : isLastSegmentInMinimap + ? "" + : "opacity-70" + : "" + } ${ + isFirstSegmentInMinimap || isLastSegmentInMinimap + ? "relative h-2 border-b-2 border-gray-500" + : "" + }`; + + const severityColors: { [key: number]: string } = { + 1: reviewed + ? "from-severity_motion-dimmed/50 to-severity_motion/50" + : "from-severity_motion-dimmed to-severity_motion", + 2: reviewed + ? "from-severity_detection-dimmed/50 to-severity_detection/50" + : "from-severity_detection-dimmed to-severity_detection", + 3: reviewed + ? "from-severity_alert-dimmed/50 to-severity_alert/50" + : "from-severity_alert-dimmed to-severity_alert", + }; + + const segmentClick = useCallback(() => { + if (contentRef.current && startTimestamp) { + const element = contentRef.current.querySelector( + `[data-segment-start="${startTimestamp - segmentDuration}"]`, + ); + if (element instanceof HTMLElement) { + scrollIntoView(element, { + scrollMode: "if-needed", + behavior: "smooth", + }); + element.classList.add( + `outline-severity_${severityType}`, + `shadow-severity_${severityType}`, + ); + element.classList.add("outline-4", "shadow-[0_0_6px_1px]"); + element.classList.remove("outline-0", "shadow-none"); + + // Remove the classes after a short timeout + setTimeout(() => { + element.classList.remove("outline-4", "shadow-[0_0_6px_1px]"); + element.classList.add("outline-0", "shadow-none"); + }, 3000); + } + } + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [startTimestamp]); + + return ( +
+ + + + + + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ + {severity.map((severityValue: number, index: number) => ( + +
+
+
+
+ ))} +
+ ); +} + +export default MotionSegment; diff --git a/web/src/components/timeline/ReviewTimeline.tsx b/web/src/components/timeline/ReviewTimeline.tsx new file mode 100644 index 000000000..bd5fdf4dd --- /dev/null +++ b/web/src/components/timeline/ReviewTimeline.tsx @@ -0,0 +1,84 @@ +import { ReactNode, RefObject } from "react"; + +export type ReviewTimelineProps = { + timelineRef: RefObject; + scrollTimeRef: RefObject; + handlebarTimeRef: RefObject; + handleMouseMove: ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => void; + handleMouseUp: ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => void; + handleMouseDown: ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => void; + segmentDuration: number; + showHandlebar: boolean; + isDragging: boolean; + children: ReactNode; +}; + +export function ReviewTimeline({ + timelineRef, + scrollTimeRef, + handlebarTimeRef, + handleMouseMove, + handleMouseUp, + handleMouseDown, + segmentDuration, + showHandlebar = false, + isDragging, + children, +}: ReviewTimelineProps) { + return ( +
+
{children}
+ {showHandlebar && ( +
+
+
+
+
+
+
+
+
+
+ )} +
+ ); +} + +export default ReviewTimeline; diff --git a/web/src/components/timeline/segment-metadata.tsx b/web/src/components/timeline/segment-metadata.tsx new file mode 100644 index 000000000..e7015b9e8 --- /dev/null +++ b/web/src/components/timeline/segment-metadata.tsx @@ -0,0 +1,100 @@ +import { isDesktop } from "react-device-detect"; + +type MinimapSegmentProps = { + isFirstSegmentInMinimap: boolean; + isLastSegmentInMinimap: boolean; + alignedMinimapStartTime: number; + alignedMinimapEndTime: number; + firstMinimapSegmentRef: React.MutableRefObject; +}; + +type TickSegmentProps = { + timestamp: Date; + timestampSpread: number; +}; + +type TimestampSegmentProps = { + isFirstSegmentInMinimap: boolean; + isLastSegmentInMinimap: boolean; + timestamp: Date; + timestampSpread: number; + segmentKey: number; +}; + +export function MinimapBounds({ + isFirstSegmentInMinimap, + isLastSegmentInMinimap, + alignedMinimapStartTime, + alignedMinimapEndTime, + firstMinimapSegmentRef, +}: MinimapSegmentProps) { + return ( + <> + {isFirstSegmentInMinimap && ( +
+ {new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + ...(isDesktop && { month: "short", day: "2-digit" }), + })} +
+ )} + + {isLastSegmentInMinimap && ( +
+ {new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + ...(isDesktop && { month: "short", day: "2-digit" }), + })} +
+ )} + + ); +} + +export function Tick({ timestamp, timestampSpread }: TickSegmentProps) { + return ( +
+
+
+
+
+ ); +} + +export function Timestamp({ + isFirstSegmentInMinimap, + isLastSegmentInMinimap, + timestamp, + timestampSpread, + segmentKey, +}: TimestampSegmentProps) { + return ( +
+ {!isFirstSegmentInMinimap && !isLastSegmentInMinimap && ( +
+ {timestamp.getMinutes() % timestampSpread === 0 && + timestamp.getSeconds() === 0 && + timestamp.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} +
+ )} +
+ ); +} diff --git a/web/src/hooks/use-segment-utils.ts b/web/src/hooks/use-event-segment-utils.ts similarity index 99% rename from web/src/hooks/use-segment-utils.ts rename to web/src/hooks/use-event-segment-utils.ts index 0cc45adb1..8901835f0 100644 --- a/web/src/hooks/use-segment-utils.ts +++ b/web/src/hooks/use-event-segment-utils.ts @@ -1,7 +1,7 @@ import { useCallback, useMemo } from "react"; import { ReviewSegment } from "@/types/review"; -export const useSegmentUtils = ( +export const useEventSegmentUtils = ( segmentDuration: number, events: ReviewSegment[], severityType: string, diff --git a/web/src/hooks/use-motion-segment-utils.ts b/web/src/hooks/use-motion-segment-utils.ts new file mode 100644 index 000000000..b9b2ce665 --- /dev/null +++ b/web/src/hooks/use-motion-segment-utils.ts @@ -0,0 +1,76 @@ +import { useCallback } from "react"; +import { MockMotionData } from "@/pages/UIPlayground"; + +export const useMotionSegmentUtils = ( + segmentDuration: number, + motion_events: MockMotionData[], +) => { + const getSegmentStart = useCallback( + (time: number): number => { + return Math.floor(time / segmentDuration) * segmentDuration; + }, + [segmentDuration], + ); + + const getSegmentEnd = useCallback( + (time: number | undefined): number => { + if (time) { + return ( + Math.floor(time / segmentDuration) * segmentDuration + segmentDuration + ); + } else { + return Date.now() / 1000 + segmentDuration; + } + }, + [segmentDuration], + ); + + const interpolateMotionAudioData = useCallback( + ( + value: number, + oldMin: number, + oldMax: number, + newMin: number, + newMax: number, + ): number => { + return ( + ((value - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin + ); + }, + [], + ); + + const getMotionSegmentValue = useCallback( + (time: number): number => { + const matchingEvent = motion_events.find((event) => { + return ( + time >= getSegmentStart(event.start_time) && + time < getSegmentEnd(event.end_time) + ); + }); + + return matchingEvent?.motionValue ?? 0; + }, + [motion_events, getSegmentStart, getSegmentEnd], + ); + + const getAudioSegmentValue = useCallback( + (time: number): number => { + const matchingEvent = motion_events.find((event) => { + return ( + time >= getSegmentStart(event.start_time) && + time < getSegmentEnd(event.end_time) + ); + }); + + return matchingEvent?.audioValue ?? 0; + }, + [motion_events, getSegmentStart, getSegmentEnd], + ); + + return { + getMotionSegmentValue, + getAudioSegmentValue, + interpolateMotionAudioData, + }; +}; diff --git a/web/src/pages/UIPlayground.tsx b/web/src/pages/UIPlayground.tsx index d91c3d137..10306e07e 100644 --- a/web/src/pages/UIPlayground.tsx +++ b/web/src/pages/UIPlayground.tsx @@ -7,6 +7,16 @@ import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; import { ReviewData, ReviewSegment, ReviewSeverity } from "@/types/review"; import { Button } from "@/components/ui/button"; import CameraActivityIndicator from "@/components/indicators/CameraActivityIndicator"; +import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; // Color data const colors = [ @@ -43,6 +53,39 @@ function ColorSwatch({ name, value }: { name: string; value: string }) { ); } +export type MockMotionData = { + start_time: number; + end_time: number; + motionValue: number; + audioValue: number; +}; + +function generateRandomMotionAudioData(): MockMotionData[] { + const now = new Date(); + const endTime = now.getTime() / 1000; + const startTime = endTime - 24 * 60 * 60; // 24 hours ago + const interval = 30; // 30 seconds + + const data = []; + for ( + let startTimestamp = startTime; + startTimestamp < endTime; + startTimestamp += interval + ) { + const endTimestamp = startTimestamp + interval; + const motionValue = Math.floor(Math.random() * 101); // Random number between 0 and 100 + const audioValue = Math.random() * -100; // Random negative value between -100 and 0 + data.push({ + start_time: startTimestamp, + end_time: endTimestamp, + motionValue, + audioValue, + }); + } + + return data; +} + const generateRandomEvent = (): ReviewSegment => { const start_time = Math.floor(Date.now() / 1000) - 10800 - Math.random() * 60 * 60; @@ -83,6 +126,7 @@ function UIPlayground() { const { data: config } = useSWR("config"); const contentRef = useRef(null); const [mockEvents, setMockEvents] = useState([]); + const [mockMotionData, setMockMotionData] = useState([]); const [handlebarTime, setHandlebarTime] = useState( Math.floor(Date.now() / 1000) - 15 * 60, ); @@ -90,6 +134,7 @@ function UIPlayground() { useMemo(() => { const initialEvents = Array.from({ length: 50 }, generateRandomEvent); setMockEvents(initialEvents); + setMockMotionData(generateRandomMotionAudioData()); }, []); // Calculate minimap start and end times based on events @@ -142,6 +187,8 @@ function UIPlayground() { setIsDragging(dragging); }; + const [isEventsReviewTimeline, setIsEventsReviewTimeline] = useState(true); + return ( <>
@@ -175,6 +222,28 @@ function UIPlayground() {

Handlebar is dragging: {isDragging ? "yes" : "no"}

+
+ Timeline type + +
@@ -215,23 +284,44 @@ function UIPlayground() {
-
- +
+ {!isEventsReviewTimeline && ( + + )} + {isEventsReviewTimeline && ( + + )}
diff --git a/web/tailwind.config.js b/web/tailwind.config.js index f87d4538f..d4078c18e 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -91,6 +91,12 @@ module.exports = { DEFAULT: "hsl(var(--severity_motion))", dimmed: "hsl(var(--severity_motion_dimmed))", }, + motion_review: { + DEFAULT: "hsl(var(--motion_review))", + }, + audio_review: { + DEFAULT: "hsl(var(--audio_review))", + }, }, keyframes: { "accordion-down": { diff --git a/web/themes/theme-default.css b/web/themes/theme-default.css index 32e9d2288..83f40870f 100644 --- a/web/themes/theme-default.css +++ b/web/themes/theme-default.css @@ -73,6 +73,12 @@ --severity_motion: var(--yellow-400); --severity_motion_dimmed: var(--yellow-200); + + --motion_review: hsl(44, 94%, 50%); + --motion_review: 44, 94%, 50%; + + --audio_review: hsl(228, 94%, 67%); + --audio_review: 228, 94%, 67%; } .dark {