From d7cc7f20dff8683780ab4d79ccd87ced53b5e3db Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 7 May 2026 11:51:26 +0200 Subject: [PATCH 1/5] refactor: add RundownUtils.formatDiffToTimecodeHours helper (7 call sites) --- packages/webui/src/client/lib/rundown.ts | 5 +++++ packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx | 2 +- .../webui/src/client/ui/RundownList/RundownListItemView.tsx | 4 ++-- .../webui/src/client/ui/RundownList/RundownPlaylistUi.tsx | 4 ++-- .../webui/src/client/ui/RundownView/RundownDividerHeader.tsx | 2 +- .../ui/RundownView/RundownHeader/RundownHeaderTimers.tsx | 2 +- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/webui/src/client/lib/rundown.ts b/packages/webui/src/client/lib/rundown.ts index 0e99d5d45a..1c524aaca9 100644 --- a/packages/webui/src/client/lib/rundown.ts +++ b/packages/webui/src/client/lib/rundown.ts @@ -265,6 +265,11 @@ export namespace RundownUtils { ) } + /** Format a duration (ms) to a timecode string, showing hours only when needed. e.g. "23:45" or "1:23:45" */ + export function formatDiffToTimecodeHours(milliseconds: number): string { + return formatDiffToTimecode(milliseconds, false, true, true, false, true) + } + export function isInsideViewport( scrollLeft: number, scrollWidth: number, diff --git a/packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx b/packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx index 3ff4cc99ba..17bc1d0d07 100644 --- a/packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx +++ b/packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx @@ -18,7 +18,7 @@ export function TTimerDisplay({ timer }: Readonly): JSX.Elem const diff = calculateTTimerDiff(timer, now) const overUnder = calculateTTimerOverUnder(timer, now) - const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(diff), false, true, true, false, true) + const timeStr = RundownUtils.formatDiffToTimecodeHours(Math.abs(diff)) const timerSign = diff >= 0 ? '' : '-' return ( diff --git a/packages/webui/src/client/ui/RundownList/RundownListItemView.tsx b/packages/webui/src/client/ui/RundownList/RundownListItemView.tsx index 8b3328a291..9aa346852f 100644 --- a/packages/webui/src/client/ui/RundownList/RundownListItemView.tsx +++ b/packages/webui/src/client/ui/RundownList/RundownListItemView.tsx @@ -147,14 +147,14 @@ export default React.memo(function RundownListItemView({ > {t('({{timecode}})', { - timecode: RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, false, true), + timecode: RundownUtils.formatDiffToTimecodeHours(expectedDuration), })}   ) : ( - RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, false, true) + RundownUtils.formatDiffToTimecodeHours(expectedDuration) ) ) : isOnlyRundownInPlaylist && isLoopDefined(playlist) ? ( {t('({{timecode}})', { - timecode: RundownUtils.formatDiffToTimecode(playlistExpectedDuration, false, true, true, false, true), + timecode: RundownUtils.formatDiffToTimecodeHours(playlistExpectedDuration), })}   ) : ( - RundownUtils.formatDiffToTimecode(playlistExpectedDuration, false, true, true, false, true) + RundownUtils.formatDiffToTimecodeHours(playlistExpectedDuration) )) return ( diff --git a/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx index 6b0551b8ec..2689a35790 100644 --- a/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx @@ -86,7 +86,7 @@ export function RundownDividerHeader({ rundown, playlist }: IProps): JSX.Element {expectedDuration ? (
{t('Planned Duration')}  - {RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, false, true)} + {RundownUtils.formatDiffToTimecodeHours(expectedDuration)}
) : null} {expectedEnd ? ( diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index 176eb08a25..65dd18919a 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -40,7 +40,7 @@ function SingleTimer({ timer }: Readonly) { const diff = calculateTTimerDiff(timer, now) const overUnder = calculateTTimerOverUnder(timer, now) - const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(diff), false, true, true, false, true) + const timeStr = RundownUtils.formatDiffToTimecodeHours(Math.abs(diff)) const isCountingDown = mode.type === 'countdown' && diff < 0 && isRunning return ( From ea6b20e29409d98de6719f61d2e27bf8830184c9 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 7 May 2026 11:53:59 +0200 Subject: [PATCH 2/5] refactor: add RundownUtils.formatDiffToTimecodeCountdown helper (6 call sites) --- packages/webui/src/client/lib/rundown.ts | 8 ++++++++ packages/webui/src/client/ui/ClockView/Timediff.tsx | 2 +- .../RundownHeader/CurrentPartOrSegmentRemaining.tsx | 6 +++--- .../ui/RundownView/RundownTiming/CurrentPartElapsed.tsx | 2 +- .../webui/src/client/ui/Shelf/PieceCountdownPanel.tsx | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/webui/src/client/lib/rundown.ts b/packages/webui/src/client/lib/rundown.ts index 1c524aaca9..cef1ade3d5 100644 --- a/packages/webui/src/client/lib/rundown.ts +++ b/packages/webui/src/client/lib/rundown.ts @@ -270,6 +270,14 @@ export namespace RundownUtils { return formatDiffToTimecode(milliseconds, false, true, true, false, true) } + /** + * Format a live countdown timer (ms). Shows "+" for positive, blank prefix when negative (rounds toward zero), + * using hard-floor rounding so the display doesn't jump ahead of the actual value. + */ + export function formatDiffToTimecodeCountdown(milliseconds: number): string { + return formatDiffToTimecode(milliseconds, true, false, true, false, true, '', false, true) + } + export function isInsideViewport( scrollLeft: number, scrollWidth: number, diff --git a/packages/webui/src/client/ui/ClockView/Timediff.tsx b/packages/webui/src/client/ui/ClockView/Timediff.tsx index 2f6b93b265..928754add6 100644 --- a/packages/webui/src/client/ui/ClockView/Timediff.tsx +++ b/packages/webui/src/client/ui/ClockView/Timediff.tsx @@ -4,7 +4,7 @@ import { RundownUtils } from '../../lib/rundown.js' export function Timediff({ time: rawTime }: { time: number }): JSX.Element { const time = -rawTime const isNegative = Math.floor(time / 1000) > 0 - const timeString = RundownUtils.formatDiffToTimecode(time, true, false, true, false, true, '', false, true) + const timeString = RundownUtils.formatDiffToTimecodeCountdown(time) return ( = (pro className={ClassNames(props.className, Math.floor(displayTimecode / 1000) > 0 ? props.heavyClassName : undefined)} role="timer" > - {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} + {RundownUtils.formatDiffToTimecodeCountdown(displayTimecode || 0)} ) } @@ -166,7 +166,7 @@ export const RundownHeaderPartRemaining: React.FC = (props) label={props.label} className={ClassNames(props.className, Math.floor(displayTimecode / 1000) > 0 ? props.heavyClassName : undefined)} > - {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} + {RundownUtils.formatDiffToTimecodeCountdown(displayTimecode || 0)} ) } @@ -187,7 +187,7 @@ export const RundownHeaderSegmentBudget: React.FC<{ {label} 0 ? 'overtime' : undefined)}> - {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} + {RundownUtils.formatDiffToTimecodeCountdown(displayTimecode || 0)} ) diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/CurrentPartElapsed.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/CurrentPartElapsed.tsx index f69443b985..f69a2be1b5 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/CurrentPartElapsed.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/CurrentPartElapsed.tsx @@ -19,7 +19,7 @@ export function CurrentPartElapsed({ currentPartId, className }: IPartElapsedPro return ( - {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} + {RundownUtils.formatDiffToTimecodeCountdown(displayTimecode || 0)} ) } diff --git a/packages/webui/src/client/ui/Shelf/PieceCountdownPanel.tsx b/packages/webui/src/client/ui/Shelf/PieceCountdownPanel.tsx index ec1e4335e9..e5b3e8d55e 100644 --- a/packages/webui/src/client/ui/Shelf/PieceCountdownPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/PieceCountdownPanel.tsx @@ -83,7 +83,7 @@ export function PieceCountdownPanel({ overtime: Math.floor(displayTimecode / 1000) > 0, })} > - {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} + {RundownUtils.formatDiffToTimecodeCountdown(displayTimecode || 0)} ) From 9faa1775c946bd0a73e5fdda5a53fcb2d04622d7 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 7 May 2026 11:54:56 +0200 Subject: [PATCH 3/5] refactor: add RundownUtils.formatDiffToTimecodeWithSign helper (5 call sites) --- packages/webui/src/client/lib/rundown.ts | 8 ++++++++ .../ui/RundownView/RundownTiming/PartDuration.tsx | 6 +++--- .../webui/src/client/ui/SegmentList/LinePart.tsx | 12 +----------- .../Renderers/VTThumbnailRenderer.tsx | 4 +--- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/webui/src/client/lib/rundown.ts b/packages/webui/src/client/lib/rundown.ts index cef1ade3d5..b03161559a 100644 --- a/packages/webui/src/client/lib/rundown.ts +++ b/packages/webui/src/client/lib/rundown.ts @@ -278,6 +278,14 @@ export namespace RundownUtils { return formatDiffToTimecode(milliseconds, true, false, true, false, true, '', false, true) } + /** + * Format a budget/remaining duration (ms) with a "+" prefix for positive values (time remaining) + * and an en-dash for negative values (over budget). + */ + export function formatDiffToTimecodeWithSign(milliseconds: number): string { + return formatDiffToTimecode(milliseconds, false, false, true, false, true, '+') + } + export function isInsideViewport( scrollLeft: number, scrollWidth: number, diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/PartDuration.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/PartDuration.tsx index b3200d436b..1d7793465f 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/PartDuration.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/PartDuration.tsx @@ -48,15 +48,15 @@ export function PartDisplayDuration(props: IPartDurationProps): JSX.Element | nu {props.label} {props.fixed ? ( - {RundownUtils.formatDiffToTimecode(budget, false, false, true, false, true, '+')} + {RundownUtils.formatDiffToTimecodeWithSign(budget)} ) : props.countUp ? ( - {RundownUtils.formatDiffToTimecode(playedOut, false, false, true, false, true, '+')} + {RundownUtils.formatDiffToTimecodeWithSign(playedOut)} ) : ( - {RundownUtils.formatDiffToTimecode(duration, false, false, true, false, true, '+')} + {RundownUtils.formatDiffToTimecodeWithSign(duration)} )} diff --git a/packages/webui/src/client/ui/SegmentList/LinePart.tsx b/packages/webui/src/client/ui/SegmentList/LinePart.tsx index 6a30af3d0a..2fc8994942 100644 --- a/packages/webui/src/client/ui/SegmentList/LinePart.tsx +++ b/packages/webui/src/client/ui/SegmentList/LinePart.tsx @@ -112,17 +112,7 @@ export function LinePart({ return ( <> {part.instance.part.expectedDuration !== undefined && part.instance.part.expectedDuration > 0 && ( - - {RundownUtils.formatDiffToTimecode( - part.instance.part.expectedDuration, - false, - false, - true, - false, - true, - '+' - )} - + {RundownUtils.formatDiffToTimecodeWithSign(part.instance.part.expectedDuration)} )} {(part.instance.part.expectedDuration === 0 || part.instance.part.expectedDuration === undefined) && ( ––:–– diff --git a/packages/webui/src/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/VTThumbnailRenderer.tsx b/packages/webui/src/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/VTThumbnailRenderer.tsx index d01d4752a7..aba1a82365 100644 --- a/packages/webui/src/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/VTThumbnailRenderer.tsx +++ b/packages/webui/src/client/ui/SegmentStoryboard/StoryboardPartThumbnail/Renderers/VTThumbnailRenderer.tsx @@ -97,9 +97,7 @@ function VTThumbnailRendererWithTiming({ > - {contentLeft > 0 ? ( - {RundownUtils.formatDiffToTimecode(contentLeft, false, false, true, false, true, '+')} - ) : null} + {contentLeft > 0 ? {RundownUtils.formatDiffToTimecodeWithSign(contentLeft)} : null} ) : null } From 91622170660c3aaaf5b1340756473bbf724f86ad Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 7 May 2026 11:56:05 +0200 Subject: [PATCH 4/5] refactor: add RundownUtils.formatDiffToTimecodeOverUnder helper (4 call sites) --- packages/webui/src/client/lib/rundown.ts | 8 ++++++++ .../ui/RundownView/RundownTiming/RundownName.tsx | 13 ++----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/webui/src/client/lib/rundown.ts b/packages/webui/src/client/lib/rundown.ts index b03161559a..6a0acbf03e 100644 --- a/packages/webui/src/client/lib/rundown.ts +++ b/packages/webui/src/client/lib/rundown.ts @@ -286,6 +286,14 @@ export namespace RundownUtils { return formatDiffToTimecode(milliseconds, false, false, true, false, true, '+') } + /** + * Format an over/under diff (ms) with "+" for positive, en-dash for negative, smart-floor rounding, + * and smart-hours. Used for start-time diffs and over/under clocks. + */ + export function formatDiffToTimecodeOverUnder(milliseconds: number): string { + return formatDiffToTimecode(milliseconds, true, false, true, true, true) + } + export function isInsideViewport( scrollLeft: number, scrollWidth: number, diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx index d41e581732..af16785576 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx @@ -76,17 +76,8 @@ export function RundownName({ )} {!hideDiff && rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal - ? expectedStart && - RundownUtils.formatDiffToTimecode( - rundownPlaylist.startedPlayback - expectedStart, - true, - false, - true, - true, - true - ) - : expectedStart && - RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)} + ? expectedStart && RundownUtils.formatDiffToTimecodeOverUnder(rundownPlaylist.startedPlayback - expectedStart) + : expectedStart && RundownUtils.formatDiffToTimecodeOverUnder(getCurrentTime() - expectedStart)} ) } From c8cbca2b310ab5bf1f64696822167f604924e774 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 7 May 2026 12:11:02 +0200 Subject: [PATCH 5/5] refactor: add floorTime param to formatDiffToTimecodeOverUnder, replace 4 call sites --- .../webui/src/client/lib/Components/CounterComponents.tsx | 6 +++--- packages/webui/src/client/lib/rundown.ts | 5 +++-- .../ui/RundownView/RundownTiming/PlaylistEndTiming.tsx | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/webui/src/client/lib/Components/CounterComponents.tsx b/packages/webui/src/client/lib/Components/CounterComponents.tsx index 504969259d..fec18f2f66 100644 --- a/packages/webui/src/client/lib/Components/CounterComponents.tsx +++ b/packages/webui/src/client/lib/Components/CounterComponents.tsx @@ -9,7 +9,7 @@ export const OverUnderClockComponent = (props: OverUnderProps): JSX.Element => { return (
- {RundownUtils.formatDiffToTimecode(props.value, true, false, true, true, true, undefined, true, true)} + {RundownUtils.formatDiffToTimecodeOverUnder(props.value, true)}
) @@ -26,7 +26,7 @@ export const PlannedEndComponent = (props: OverUnderProps): JSX.Element => { export const TimeToPlannedEndComponent = (props: OverUnderProps): JSX.Element => { return ( - {RundownUtils.formatDiffToTimecode(props.value, true, false, true, true, true, undefined, true, true)} + {RundownUtils.formatDiffToTimecodeOverUnder(props.value, true)} ) } @@ -34,7 +34,7 @@ export const TimeToPlannedEndComponent = (props: OverUnderProps): JSX.Element => export const TimeSincePlannedEndComponent = (props: OverUnderProps): JSX.Element => { return ( - {RundownUtils.formatDiffToTimecode(props.value, true, false, true, true, true, undefined, true, true)} + {RundownUtils.formatDiffToTimecodeOverUnder(props.value, true)} ) } diff --git a/packages/webui/src/client/lib/rundown.ts b/packages/webui/src/client/lib/rundown.ts index 6a0acbf03e..700c506131 100644 --- a/packages/webui/src/client/lib/rundown.ts +++ b/packages/webui/src/client/lib/rundown.ts @@ -289,9 +289,10 @@ export namespace RundownUtils { /** * Format an over/under diff (ms) with "+" for positive, en-dash for negative, smart-floor rounding, * and smart-hours. Used for start-time diffs and over/under clocks. + * Pass `floorTime: true` to use hard-floor rounding (display won't jump ahead of the actual value). */ - export function formatDiffToTimecodeOverUnder(milliseconds: number): string { - return formatDiffToTimecode(milliseconds, true, false, true, true, true) + export function formatDiffToTimecodeOverUnder(milliseconds: number, floorTime?: boolean): string { + return formatDiffToTimecode(milliseconds, true, false, true, true, true, undefined, floorTime, floorTime) } export function isInsideViewport( diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx index 5cc865342d..12158d57fa 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx @@ -55,7 +55,7 @@ export function PlaylistEndTiming({ role="timer" > {!hideDiffLabel && {t('Diff')}} - {RundownUtils.formatDiffToTimecode(overUnderClock, true, false, true, true, true, undefined, true, true)} + {RundownUtils.formatDiffToTimecodeOverUnder(overUnderClock, true)} ) : null ) : null}