From 60cdf45c369d65438ea69ad01da1d308c7a5bc58 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Wed, 26 Aug 2026 12:32:27 +0000 Subject: [PATCH] feat(ui): render a live timer in the message control preview The preview showed the word "Timer" where the timer goes, so it could not tell an operator what the audience will actually see. It now embeds the same component the picture-in-picture window uses. Making PipTimer embeddable also fixes the pop-out window, which rendered a running timer while the stage was blacked out and did not blink with the stage. The timer font size, the message padding and the overtime outline move from viewport to container units: inside the preview they resolve against the stage frame, and in the pip window - which has no ancestor query container - they resolve against its viewport exactly as before. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011CHxSQ6nWHuyar8feieRra --- apps/client/src/common/hooks/useSocket.ts | 5 -- .../control/message/TimerPreview.module.scss | 45 ++++++++-------- .../features/control/message/TimerPreview.tsx | 54 +++---------------- .../src/views/editor/pip-timer/PipTimer.scss | 29 +++++++--- .../src/views/editor/pip-timer/PipTimer.tsx | 6 ++- 5 files changed, 55 insertions(+), 84 deletions(-) diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index b64cce05f..5009b612e 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -39,11 +39,6 @@ export const useExternalMessageInput = createSelector((state: RuntimeStore) => ( })); export const useMessagePreview = createSelector((state: RuntimeStore) => ({ - blink: state.message.timer.blink, - blackout: state.message.timer.blackout, - phase: state.timer.phase, - secondarySource: state.message.timer.secondarySource, - showTimerMessage: state.message.timer.visible && Boolean(state.message.timer.text), timerType: state.eventNow?.timerType ?? null, countToEnd: state.eventNow?.countToEnd ?? false, })); diff --git a/apps/client/src/features/control/message/TimerPreview.module.scss b/apps/client/src/features/control/message/TimerPreview.module.scss index 7f64c4856..02ae9635b 100644 --- a/apps/client/src/features/control/message/TimerPreview.module.scss +++ b/apps/client/src/features/control/message/TimerPreview.module.scss @@ -1,34 +1,33 @@ .preview { - background-color: $ui-black; - display: grid; - place-content: center; - text-align: center; position: relative; + display: grid; + place-items: center; + min-height: 0; + overflow: hidden; } -.mainContent { - font-size: 1rem; - font-weight: 600; +/* the frame is a query container so that the embedded timer scales to the preview, not to the viewport */ +.stageFrame { + position: relative; + container-type: inline-size; + contain: paint; + overflow: hidden; + aspect-ratio: 16 / 9; width: 100%; - color: var(--override-colour, $ui-white); - - &[data-phase='pending'] { - color: $ontime-roll; - } - &[data-phase='overtime'] { - color: $playback-negative; - } - &[data-phase='none'] { - opacity: $opacity-disabled; - } + max-height: 100%; + border-radius: $component-border-radius-md; } -.secondaryContent { - border-top: 1px solid $white-7; -} +/* keep the pop-out affordances reachable above the blackout and message overlays */ +.stageCorners { + position: absolute; + inset: 0; + z-index: calc($zindex-floating + 2); + pointer-events: none; -.blackout { - display: none; + > * { + pointer-events: auto; + } } .eventStatus { diff --git a/apps/client/src/features/control/message/TimerPreview.tsx b/apps/client/src/features/control/message/TimerPreview.tsx index 26847849e..9fcde5b13 100644 --- a/apps/client/src/features/control/message/TimerPreview.tsx +++ b/apps/client/src/features/control/message/TimerPreview.tsx @@ -1,4 +1,4 @@ -import { TimerPhase, TimerType } from 'ontime-types'; +import { TimerType } from 'ontime-types'; import { IoArrowDown, IoArrowUp, IoBan, IoTime } from 'react-icons/io5'; import { LuArrowDownToLine } from 'react-icons/lu'; @@ -7,62 +7,22 @@ import Tooltip from '../../../common/components/tooltip/Tooltip'; import useViewSettings from '../../../common/hooks-query/useViewSettings'; import { useMessagePreview } from '../../../common/hooks/useSocket'; import { handleLinks } from '../../../common/utils/linkUtils'; -import { cx, timerPlaceholder } from '../../../common/utils/styleUtils'; import PipRoot from '../../../views/editor/pip-timer/PipRoot'; +import { PipTimer } from '../../../views/editor/pip-timer/PipTimer'; import style from './TimerPreview.module.scss'; -const secondarySourceLabels: Record = { - aux1: 'Aux 1', - aux2: 'Aux 2', - aux3: 'Aux 3', - secondary: 'Secondary message', -}; - export default function TimerPreview() { - const { blink, blackout, countToEnd, phase, secondarySource, showTimerMessage, timerType } = useMessagePreview(); + const { countToEnd, timerType } = useMessagePreview(); const { data } = useViewSettings(); - const main = (() => { - if (showTimerMessage) return 'Message'; - if (timerType === TimerType.None) return timerPlaceholder; - if (phase === TimerPhase.Pending) return 'Standby to start'; - if (phase === TimerPhase.Overtime) return 'Timer Overtime'; - if (timerType === TimerType.Clock) return 'Clock'; - if (countToEnd) return 'Count to End'; - return 'Timer'; - })(); - - const secondary = (() => { - // message is a fullscreen overlay or secondary is not active - if (showTimerMessage || !secondarySource) return null; - - // we need to check aux first since it takes priority - return secondarySourceLabels[secondarySource]; - })(); - - const overrideColour = (() => { - // override fallback colours from starter project - if (phase === TimerPhase.Warning) return data.warningColor ?? '#ffa528'; - if (phase === TimerPhase.Danger) return data.dangerColor ?? '#ff7300'; - return data.normalColor ?? '#FFFC'; - })(); - - const showColourOverride = main == 'Timer'; - const contentClasses = cx([blink && 'blink', blackout && style.blackout]); - return (
- handleLinks('timer', event)} pipElement={} /> -
-
- {main} +
+ +
+ handleLinks('timer', event)} pipElement={} />
- {secondary !== null &&
{secondary}
}
+
+
{message.timer.text}
-
+
{display}