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