refactor: timeline design review

This commit is contained in:
Carlos Valente
2025-07-14 08:55:45 +02:00
parent 55e34b5f9a
commit 6f9e13948d
4 changed files with 63 additions and 41 deletions
@@ -1,4 +1,5 @@
import { RefObject, useCallback, useEffect } from 'react';
import { EntryId } from 'ontime-types';
function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
componentRef: RefObject<ComponentRef> | null,
@@ -26,7 +27,7 @@ interface UseHorizontalFollowComponentOptions {
followRef: RefObject<HTMLElement | null>;
scrollRef: RefObject<HTMLElement | null>;
doFollow: boolean;
hasSelectedElement?: boolean;
selectedEventId: EntryId | null;
leftOffset?: number;
setScrollFlag?: (newValue: boolean) => void;
}
@@ -39,7 +40,7 @@ export default function useHorizontalFollowComponent({
followRef,
scrollRef,
doFollow,
hasSelectedElement,
selectedEventId,
leftOffset = 0,
setScrollFlag,
}: UseHorizontalFollowComponentOptions) {
@@ -52,25 +53,25 @@ export default function useHorizontalFollowComponent({
// Use requestAnimationFrame to ensure the component is fully loaded
window.requestAnimationFrame(() => {
scrollToComponent(
hasSelectedElement ? (followRef as RefObject<HTMLElement>) : null,
selectedEventId !== null ? (followRef as RefObject<HTMLElement>) : null,
scrollRef as RefObject<HTMLElement>,
leftOffset,
);
setScrollFlag?.(false);
});
}, [followRef, scrollRef, doFollow, hasSelectedElement, leftOffset, setScrollFlag]);
}, [followRef, scrollRef, doFollow, leftOffset, setScrollFlag, selectedEventId]);
const scrollToRefComponent = useCallback(
(componentRef = followRef, containerRef = scrollRef, offset = leftOffset) => {
if (containerRef.current) {
scrollToComponent(
hasSelectedElement ? (componentRef as RefObject<HTMLElement>) : null,
selectedEventId !== null ? (componentRef as RefObject<HTMLElement>) : null,
containerRef as RefObject<HTMLElement>,
offset,
);
}
},
[followRef, scrollRef, hasSelectedElement, leftOffset],
[followRef, scrollRef, leftOffset, selectedEventId],
);
return scrollToRefComponent;
@@ -28,6 +28,13 @@ $timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-over
height: 100%;
}
.maybeInline {
display: flex;
align-items: center;
gap: 0.5em;
flex-wrap: wrap;
}
// generate combined timeline
.timelineBlock {
height: $timeline-height;
@@ -65,7 +72,7 @@ $timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-over
flex: 1;
display: flex;
flex-direction: column;
gap: 2rem;
gap: 1rem;
padding-top: 0.25rem;
padding-inline-start: 0.25rem;
overflow: hidden;
@@ -76,32 +83,15 @@ $timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-over
border-top: 2px solid var(--background-color-override, $viewer-background-color);
box-shadow: 0 0.25rem 0 0 var(--color, $gray-300);
text-transform: capitalize;
white-space: normal;
&[data-status='done'] {
opacity: $opacity-disabled;
}
&[data-status='live'] {
box-shadow: 0 0.25rem 0 0 $active-red;
}
}
.delay {
margin-top: -2rem;
margin-bottom: -1rem;
}
.timeOverview {
padding-top: 0.25rem;
padding-inline-start: 0.25em;
text-transform: capitalize;
white-space: normal;
height: 6rem;
&[data-status='done'] {
opacity: $opacity-disabled;
}
&[data-status='live'] {
.status {
color: $active-red;
}
@@ -114,6 +104,22 @@ $timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-over
}
}
.delay {
color: $delay-color;
}
.timeOverview {
padding-top: 0.25rem;
padding-inline-start: 0.25em;
height: 3rem;
}
.cross {
text-decoration: line-through;
text-decoration-thickness: 2px;
text-decoration-color: $delay-color;
}
.separeLeft {
border-left: 1px solid var(--color, $gray-300);
}
+2 -1
View File
@@ -38,7 +38,7 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
followRef: selectedRef,
scrollRef: scrollContainerRef,
doFollow: autosize,
hasSelectedElement: selectedEventId !== null,
selectedEventId: selectedEventId,
// No offset when hiding past events to ensure content starts at 0
leftOffset: hidePast ? 0 : screenWidth / 6,
});
@@ -92,6 +92,7 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
colour={event.colour}
delay={event.delay ?? 0}
duration={event.duration}
hasLink={Boolean(event.linkStart)}
left={position.left}
status={statusMap[event.id]}
start={event.timeStart + (event.dayOffset ?? 0) * dayInMs}
@@ -16,6 +16,7 @@ interface TimelineEntryProps {
colour: string;
delay: number;
duration: number;
hasLink: boolean;
left: number;
status: ProgressStatus;
start: number;
@@ -29,21 +30,31 @@ const formatOptions = {
format24: 'HH:mm',
};
export function TimelineEntry({ colour, delay, duration, left, status, start, title, width, ref }: TimelineEntryProps) {
export function TimelineEntry({
colour,
delay,
duration,
hasLink,
left,
status,
start,
title,
width,
ref,
}: TimelineEntryProps) {
const formattedStartTime = formatTime(start, formatOptions);
const formattedDuration = formatDuration(duration);
const delayedStart = start + delay;
const hasDelay = delay > 0;
const lighterColour = alpha(colour, 0.7);
const columnClasses = cx([style.column, width < 40 && style.smallArea]);
const contentClasses = cx([style.content, width < 20 && style.hide]);
const showTitle = width > 25;
const smallArea = width < 40;
return (
<div
ref={ref}
className={columnClasses}
className={cx([style.column, smallArea && style.smallArea])}
style={{
'--color': colour,
'--lighter': lighterColour ?? '',
@@ -53,24 +64,27 @@ export function TimelineEntry({ colour, delay, duration, left, status, start, ti
>
{status === 'live' ? <ActiveBlock /> : <div data-status={status} className={style.timelineBlock} />}
<div
className={contentClasses}
className={cx([style.content, width < 20 && style.hide, !hasLink && style.separeLeft])}
data-status={status}
style={{
'--color': colour,
}}
>
<div className={hasDelay ? style.cross : undefined}>{formattedStartTime}</div>
{hasDelay && <div className={style.delay}>{formatTime(delayedStart, formatOptions)}</div>}
{showTitle && <div>{title}</div>}
</div>
<div className={style.timeOverview} data-status={status}>
{status !== 'done' && (
<div className={style.maybeInline}>
<div className={cx([hasDelay && style.cross])}>{formattedStartTime}</div>
{hasDelay && <div className={style.delay}>{formatTime(delayedStart, formatOptions)}</div>}
{smallArea && <TimelineEntryStatus delay={delay} start={start} status={status} />}
</div>
{showTitle && (
<>
<div className={style.duration}>{formattedDuration}</div>
<TimelineEntryStatus delay={delay} start={start} status={status} />
{!smallArea && <TimelineEntryStatus delay={delay} start={start} status={status} />}
<div>{title}</div>
</>
)}
</div>
<div className={style.timeOverview} data-status={status}>
{status !== 'done' && <div className={style.duration}>{formattedDuration}</div>}
</div>
</div>
);
}