refactor: small fixes and style tweaks

fix: prevent reflow on editing images

fix: schedule overview

fix: report on all events

refactor: over-under time colours

fix: prevent edit on delete
This commit is contained in:
Carlos Valente
2025-07-14 11:45:41 +02:00
committed by Carlos Valente
parent 1e1f547ce1
commit ad2ef3b53f
22 changed files with 135 additions and 141 deletions
+2 -2
View File
@@ -18,8 +18,8 @@ export function getOffsetText(offset: MaybeNumber): string {
return offsetText; return offsetText;
} }
export function getOffsetState(offset: MaybeNumber): 'ahead' | 'behind' | 'muted' | null { export function getOffsetState(offset: MaybeNumber): 'over' | 'under' | 'muted' | null {
if (offset === null) return null; if (offset === null) return null;
if (offset === 0) return 'muted'; if (offset === 0) return 'muted';
return offset < 0 ? 'behind' : 'ahead'; return offset < 0 ? 'over' : 'under';
} }
@@ -1,7 +1,7 @@
th.over { th.over {
color: $ontime-delay-text; color: $playback-over;
} }
th.under { th.under {
color: $playback-ahead; color: $playback-under;
} }
@@ -29,8 +29,8 @@ export default function ReportSettings() {
}; };
const combinedReport = useMemo(() => { const combinedReport = useMemo(() => {
return getCombinedReport(reportData, data.entries, data.order); return getCombinedReport(reportData, data.entries, data.flatOrder);
}, [reportData, data.entries, data.order]); }, [reportData, data.entries, data.flatOrder]);
return ( return (
<Panel.Section> <Panel.Section>
@@ -82,7 +82,7 @@ export default function ReportSettings() {
return 'over'; return 'over';
})(); })();
return ( return (
<tr key={entry.index}> <tr key={entry.id}>
<th>{entry.index}</th> <th>{entry.index}</th>
<th>{entry.cue}</th> <th>{entry.cue}</th>
<th>{entry.title}</th> <th>{entry.title}</th>
@@ -4,6 +4,7 @@ import { makeCSVFromArrayOfArrays } from '../../../../common/utils/csv';
import { formatTime } from '../../../../common/utils/time'; import { formatTime } from '../../../../common/utils/time';
export type CombinedReport = { export type CombinedReport = {
id: EntryId;
index: number; index: number;
title: string; title: string;
cue: string; cue: string;
@@ -16,24 +17,48 @@ export type CombinedReport = {
/** /**
* Creates a combined report with the rundown data * Creates a combined report with the rundown data
*/ */
export function getCombinedReport(report: OntimeReport, rundown: RundownEntries, order: EntryId[]): CombinedReport[] { export function getCombinedReport(
report: OntimeReport,
rundown: RundownEntries,
flatOrder: EntryId[],
): CombinedReport[] {
if (Object.keys(report).length === 0) return []; if (Object.keys(report).length === 0) return [];
if (order.length === 0) return []; if (flatOrder.length === 0) return [];
const combinedReport: CombinedReport[] = []; const combinedReport: CombinedReport[] = [];
for (const [key, value] of Object.entries(report)) { let index = 1;
if (!rundown[key] || !isOntimeEvent(rundown[key])) continue; for (let i = 0; i < flatOrder.length; i++) {
const id = flatOrder[i];
const entry = rundown[id];
if (!entry || !isOntimeEvent(entry)) continue;
combinedReport.push({ if (!(id in report)) {
index: order.findIndex((id) => id === key), combinedReport.push({
title: rundown[key].title, id: id,
cue: rundown[key].cue, index: index,
scheduledStart: rundown[key].timeStart, title: entry.title,
actualEnd: value.endedAt, cue: entry.cue,
scheduledEnd: rundown[key].timeEnd, scheduledStart: entry.timeStart,
actualStart: value.startedAt, actualEnd: null,
}); scheduledEnd: entry.timeEnd,
actualStart: null,
});
}
if (id in report) {
combinedReport.push({
id: id,
index: index,
title: entry.title,
cue: entry.cue,
scheduledStart: entry.timeStart,
actualEnd: report[id].endedAt,
scheduledEnd: entry.timeEnd,
actualStart: report[id].startedAt,
});
}
index++;
} }
return combinedReport; return combinedReport;
@@ -55,18 +55,6 @@
color: $muted-gray; color: $muted-gray;
} }
.offset {
color: $muted-gray;
}
.ahead {
color: $playback-ahead;
}
.behind {
color: $ontime-delay-text;
}
.labelTitle { .labelTitle {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -18,17 +18,18 @@ import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
import { formatTime, useTimeUntilStart } from '../../../common/utils/time'; import { formatTime, useTimeUntilStart } from '../../../common/utils/time';
import { calculateEndAndDaySpan, formattedTime } from '../overview.utils'; import { calculateEndAndDaySpan, formattedTime } from '../overview.utils';
import { TimeColumn } from './TimeLayout'; import { OverUnder, TimeColumn } from './TimeLayout';
import style from './TimeElements.module.scss'; import style from './TimeElements.module.scss';
export function StartTimes() { export function StartTimes() {
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview(); const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
const plannedStartText = plannedStart === null ? timerPlaceholder : formatTime(plannedStart);
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]); const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]); const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]);
const plannedEndText = maybePlannedEnd === null ? timerPlaceholder : formatTime(maybePlannedEnd);
const muted = maybeExpectedEnd === null;
return ( return (
<div className={style.column}> <div className={style.column}>
@@ -36,31 +37,31 @@ export function StartTimes() {
<span className={style.label}>Start</span> <span className={style.label}>Start</span>
<div className={style.labelledElement}> <div className={style.labelledElement}>
<Tooltip text='Planned start time' render={<TbCalendar className={style.icon} />} /> <Tooltip text='Planned start time' render={<TbCalendar className={style.icon} />} />
<span className={cx([style.time])}>{formatTime(plannedStart)}</span> <span className={cx([style.time, plannedStart === null && style.muted])}>{plannedStartText}</span>
</div> </div>
<div className={style.labelledElement}> <div className={style.labelledElement}>
<Tooltip text='Actual start time' render={<TbCalendarClock className={style.icon} />} /> <Tooltip text='Actual start time' render={<TbCalendarClock className={style.icon} />} />
<span className={cx([style.time, muted && style.muted])}>{formattedTime(actualStart)}</span> <span className={cx([style.time, actualStart === null && style.muted])}>{formattedTime(actualStart)}</span>
</div> </div>
</div> </div>
<div className={style.row}> <div className={style.row}>
<span className={style.label}>End</span> <span className={style.label}>End</span>
<div className={style.labelledElement}> <div className={style.labelledElement}>
<Tooltip text='Planned end time' render={<TbCalendar className={style.icon} />} /> <Tooltip text='Planned end time' render={<TbCalendar className={style.icon} />} />
{maybePlannedDaySpan >= 0 ? ( {maybePlannedDaySpan > 0 ? (
<Tooltip <Tooltip
text={`Event spans over ${maybePlannedDaySpan + 1} days`} text={`Event spans over ${maybePlannedDaySpan + 1} days`}
render={<span className={cx([style.time, style.daySpan])} />} render={<span className={cx([style.time, style.daySpan])} />}
> >
{formatTime(maybePlannedEnd)} {plannedEndText}
</Tooltip> </Tooltip>
) : ( ) : (
<span className={cx([style.time, muted && style.muted])}>{formatTime(maybePlannedEnd)}</span> <span className={cx([style.time, plannedEnd === null && style.muted])}>{plannedEndText}</span>
)} )}
</div> </div>
<div className={style.labelledElement}> <div className={style.labelledElement}>
<Tooltip text='Expected end time' render={<TbCalendarStar className={style.icon} />} /> <Tooltip text='Expected end time' render={<TbCalendarStar className={style.icon} />} />
{maybeExpectedEnd !== null && maybeExpectedDaySpan >= 0 ? ( {maybeExpectedEnd !== null && maybeExpectedDaySpan > 0 ? (
<Tooltip <Tooltip
text={`Event spans over ${maybeExpectedDaySpan + 1} days`} text={`Event spans over ${maybeExpectedDaySpan + 1} days`}
render={<span className={cx([style.time, style.daySpan])} />} render={<span className={cx([style.time, style.daySpan])} />}
@@ -68,7 +69,9 @@ export function StartTimes() {
{formattedTime(maybeExpectedEnd)} {formattedTime(maybeExpectedEnd)}
</Tooltip> </Tooltip>
) : ( ) : (
<span className={cx([style.time, muted && style.muted])}>{formattedTime(maybeExpectedEnd)}</span> <span className={cx([style.time, maybeExpectedEnd === null && style.muted])}>
{formattedTime(maybeExpectedEnd)}
</span>
)} )}
</div> </div>
</div> </div>
@@ -199,11 +202,10 @@ export function OffsetOverview() {
const isPlaying = isPlaybackActive(playback); const isPlaying = isPlaybackActive(playback);
const correctedOffset = offset * -1; const correctedOffset = offset * -1;
const offsetState = getOffsetState(correctedOffset); const offsetState = getOffsetState(offset);
const offsetClasses = cx([style.offset, offsetState && style[offsetState]]);
const offsetText = getOffsetText(isPlaying ? correctedOffset : null); const offsetText = getOffsetText(isPlaying ? correctedOffset : null);
return <TimeColumn label='Over / under' value={offsetText} className={offsetClasses} testId='offset' />; return <OverUnder state={offsetState} value={offsetText} testId='offset' />;
} }
export function ClockOverview() { export function ClockOverview() {
@@ -22,34 +22,31 @@
.label { .label {
line-height: 0.9em; line-height: 0.9em;
width: 7em; width: 7em;
} display: flex;
} gap: 0.25rem;
.row {
display: flex;
align-items: center;
gap: 0.5rem;
height: 2.25em;
.label {
text-align: right;
width: 5em;
} }
.clock { &[data-state="over"] {
font-size: 1.25rem; .label .over {
color: $playback-over;
}
.clock {
color: $playback-over;
}
}
&[data-state="under"] {
.label .under {
color: $playback-under;
}
.clock {
color: $playback-under;
}
}
&[data-state="muted"] {
.clock {
color: $muted-gray;
}
} }
} }
.daySpan {
&::after {
content: "*";
vertical-align: super;
font-size: 0.75em;
color: $info-blue;
}
}
.muted {
color: $muted-gray;
}
@@ -1,6 +1,3 @@
import { PropsWithChildren } from 'react';
import Tooltip from '../../../common/components/tooltip/Tooltip';
import { cx } from '../../../common/utils/styleUtils'; import { cx } from '../../../common/utils/styleUtils';
import style from './TimeLayout.module.scss'; import style from './TimeLayout.module.scss';
@@ -25,40 +22,23 @@ export function TimeColumn({ label, value, muted, className, testId }: TimeLayou
); );
} }
export function TimeRow({ label, value, daySpan, muted, className }: TimeLayoutProps) { interface OverUnderProps {
return ( state: 'over' | 'under' | 'muted' | null;
<div className={style.row}> value: string;
<span className={style.label}>{label}</span> testId: string;
{daySpan ? (
<Tooltip
text={`Event spans over ${daySpan + 1} days`}
render={<span />}
className={cx([style.clock, style.daySpan, className])}
>
{value}
</Tooltip>
) : (
<span className={cx([style.clock, muted && style.muted, className])}>{value}</span>
)}
</div>
);
} }
export function TimeElementsRow({ label, value, daySpan, muted, className }: PropsWithChildren<TimeLayoutProps>) { export function OverUnder({ state, value, testId }: OverUnderProps) {
return ( return (
<div className={style.row}> <div className={style.column} data-state={state}>
<span className={style.label}>{label}</span> <div className={style.label}>
{daySpan ? ( <span className={style.over}>Over</span>
<Tooltip <span>/</span>
text={`Event spans over ${daySpan + 1} days`} <span className={style.under}>Under</span>
render={<span />} </div>
className={cx([style.clock, style.daySpan, className])} <span className={style.clock} data-testid={testId}>
> {value}
{value} </span>
</Tooltip>
) : (
<span className={cx([style.clock, muted && style.muted, className])}>{value}</span>
)}
</div> </div>
); );
} }
@@ -8,6 +8,7 @@ import NullableTimeInput from '../../../common/components/input/time-input/Nulla
import AppLink from '../../../common/components/link/app-link/AppLink'; import AppLink from '../../../common/components/link/app-link/AppLink';
import { useEntryActions } from '../../../common/hooks/useEntryAction'; import { useEntryActions } from '../../../common/hooks/useEntryAction';
import useCustomFields from '../../../common/hooks-query/useCustomFields'; import useCustomFields from '../../../common/hooks-query/useCustomFields';
import { getOffsetState } from '../../../common/utils/offset';
import { enDash, timerPlaceholder } from '../../../common/utils/styleUtils'; import { enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
import TextLikeInput from '../../../views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput'; import TextLikeInput from '../../../views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput';
@@ -54,7 +55,7 @@ export default function BlockEditor({ block }: BlockEditorProps) {
const isEditor = window.location.pathname.includes('editor'); const isEditor = window.location.pathname.includes('editor');
const planOffset = typeof block.targetDuration !== 'number' ? null : block.duration - block.targetDuration; const planOffset = typeof block.targetDuration !== 'number' ? null : block.duration - block.targetDuration;
const planOffsetLabel = planOffset !== null && planOffset > 0 ? 'behind' : 'ahead'; const planOffsetLabel = planOffset !== null ? getOffsetState(planOffset * -1) : null;
return ( return (
<div className={style.content}> <div className={style.content}>
@@ -1,6 +1,6 @@
.imageContainer { .imageContainer {
width: 100%; width: 75px;
height: 100%; height: 75px;
background-color: $gray-1250; background-color: $gray-1250;
display: grid; display: grid;
place-content: center; place-content: center;
@@ -6,11 +6,11 @@
justify-self: end; justify-self: end;
&.over { &.over {
color: $ontime-delay-text; color: $playback-over;
} }
&.under { &.under {
color: $playback-ahead; color: $playback-under;
} }
&.due { &.due {
@@ -59,7 +59,8 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl
updateEntry({ id: entryId, [field]: value }); updateEntry({ id: entryId, [field]: value });
}; };
const handleDelete = () => { const handleDelete = (event: MouseEvent) => {
event.stopPropagation();
deleteEntry([entryId]); deleteEntry([entryId]);
}; };
+3
View File
@@ -38,6 +38,9 @@ $playback-ahead: $green-500;
$active-indicator: #8bb33d; $active-indicator: #8bb33d;
$text-black: $gray-1350; $text-black: $gray-1350;
$playback-over: #F57C13;
$playback-under: $green-500;
// interface panels // interface panels
$bg-container-l1: $gray-1350; $bg-container-l1: $gray-1350;
$bg-container-l2: $gray-1300; $bg-container-l2: $gray-1300;
@@ -50,12 +50,12 @@ $indeterminate-width: clamp(32px, 3vw, 48px);
color: $ontime-delay-text; color: $ontime-delay-text;
} }
&--ahead { &--over {
color: $green-500; color: $playback-over;
} }
&--behind { &--under {
color: $orange-500; color: $playback-under;
} }
} }
@@ -1,4 +1,5 @@
import { useRuntimeOffset } from '../../../common/hooks/useSocket'; import { useRuntimeOffset } from '../../../common/hooks/useSocket';
import { getOffsetState } from '../../../common/utils/offset';
import { cx } from '../../../common/utils/styleUtils'; import { cx } from '../../../common/utils/styleUtils';
import { formatTime } from '../../../common/utils/time'; import { formatTime } from '../../../common/utils/time';
import SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime'; import SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime';
@@ -121,11 +122,7 @@ function ProjectedTime(props: OffsetTimeProps) {
const projectedOffset = offset - delay; const projectedOffset = offset - delay;
const projectedTime = formatTime(time - offset, formatOptions); const projectedTime = formatTime(time - offset, formatOptions);
const projectedState = getOffsetState(projectedOffset);
return ( return <SuperscriptTime className={`entry-times--${projectedState}`} time={projectedTime} />;
<SuperscriptTime
className={cx([projectedOffset > 0 && 'entry-times--ahead', projectedOffset < 0 && 'entry-times--behind'])}
time={projectedTime}
/>
);
} }
@@ -141,12 +141,12 @@ $item-height: 3.5rem;
color: $delay-color; color: $delay-color;
} }
.sub__schedule--ahead { .sub__schedule--over {
color: $green-500; color: $playback-over
} }
.sub__schedule--behind { .sub__schedule--under {
color: $orange-500; color: $playback-under
} }
.sub__title { .sub__title {
@@ -7,6 +7,7 @@ import { useFadeOutOnInactivity } from '../../common/hooks/useFadeOutOnInactivit
import useFollowComponent from '../../common/hooks/useFollowComponent'; import useFollowComponent from '../../common/hooks/useFollowComponent';
import { useCurrentDay, useRuntimeOffset } from '../../common/hooks/useSocket'; import { useCurrentDay, useRuntimeOffset } from '../../common/hooks/useSocket';
import { ViewExtendedTimer } from '../../common/models/TimeManager.type'; import { ViewExtendedTimer } from '../../common/models/TimeManager.type';
import { getOffsetState } from '../../common/utils/offset';
import { cx } from '../../common/utils/styleUtils'; import { cx } from '../../common/utils/styleUtils';
import { throttle } from '../../common/utils/throttle'; import { throttle } from '../../common/utils/throttle';
import FollowButton from '../../features/operator/follow-button/FollowButton'; import FollowButton from '../../features/operator/follow-button/FollowButton';
@@ -134,14 +135,13 @@ function ProjectedSchedule(props: ProjectedScheduleProps) {
// offset is negative if we are ahead // offset is negative if we are ahead
const projectedOffset = offset - delay; const projectedOffset = offset - delay;
const projectState = getOffsetState(projectedOffset);
const classes = cx([projectedOffset > 0 && 'sub__schedule--ahead', projectedOffset < 0 && 'sub__schedule--behind']);
return ( return (
<> <>
<ClockTime <ClockTime
value={timeStart - projectedOffset} value={timeStart - projectedOffset}
className={classes} className={`sub__schedule--${projectState}`}
preferredFormat12='h:mm' preferredFormat12='h:mm'
preferredFormat24='HH:mm' preferredFormat24='HH:mm'
/> />
@@ -103,7 +103,7 @@ function DurationInput({
onClick={handleFakeFocus} onClick={handleFakeFocus}
onFocus={handleFakeFocus} onFocus={handleFakeFocus}
muted={!lockedValue} muted={!lockedValue}
offset={delayed ? 'behind' : undefined} offset={delayed ? 'over' : undefined}
ref={textRef} ref={textRef}
> >
{children} {children}
@@ -14,12 +14,12 @@
overflow: hidden; overflow: hidden;
&.ahead { &.under {
color: $playback-ahead; color: $playback-under;
} }
&.behind { &.over {
color: $ontime-delay-text; color: $playback-over;
} }
&.muted { &.muted {
@@ -5,7 +5,7 @@ import { cx } from '../../../../common/utils/styleUtils';
import style from './TextLikeInput.module.scss'; import style from './TextLikeInput.module.scss';
interface TextLikeInputProps extends HTMLAttributes<HTMLSpanElement> { interface TextLikeInputProps extends HTMLAttributes<HTMLSpanElement> {
offset?: 'ahead' | 'behind'; offset?: 'over' | 'under' | 'muted' | null;
muted?: boolean; muted?: boolean;
} }
@@ -105,7 +105,7 @@ function TimeInputDuration({
onClick={handleFakeFocus} onClick={handleFakeFocus}
onFocus={handleFakeFocus} onFocus={handleFakeFocus}
muted={!lockedValue} muted={!lockedValue}
offset={delayed ? 'behind' : undefined} offset={delayed ? 'over' : undefined}
ref={textRef} ref={textRef}
> >
{children} {children}
@@ -63,12 +63,12 @@
} }
} }
.ahead { .over {
color: $playback-ahead; color: $playback-over;
} }
.behind { .under {
color: $ontime-delay-text; color: $playback-under;
} }
.muted { .muted {