From bb8f1475756bf731c4996777ca65072fcedd20fa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 18:24:05 +0000 Subject: [PATCH] fix(teleprompter): keep the transport row fixed and label every control The follow control was rendered only while the follow could be re-engaged, so it appeared and vanished mid show and moved every control after it. An operator reaching for pause would press whatever had slid under their finger. It is now always present and goes disabled when there is nothing to re-engage, which is how the rest of the row already behaved. The controls also could not explain themselves: the follow target in particular is not a guessable icon. Each one now carries a tooltip naming the action and its key, using the shared Tooltip the editor uses. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf --- .../src/views/teleprompter/Teleprompter.tsx | 2 +- .../control-overlay/ControlOverlay.tsx | 123 ++++++++++++------ e2e/tests/features/215-teleprompter.spec.ts | 21 +++ 3 files changed, 105 insertions(+), 41 deletions(-) diff --git a/apps/client/src/views/teleprompter/Teleprompter.tsx b/apps/client/src/views/teleprompter/Teleprompter.tsx index c2f71ea2a..747576f37 100644 --- a/apps/client/src/views/teleprompter/Teleprompter.tsx +++ b/apps/client/src/views/teleprompter/Teleprompter.tsx @@ -161,7 +161,7 @@ function Teleprompter({ rundown, rundownMetadata, customFields }: TeleprompterDa void; @@ -23,11 +25,16 @@ interface ControlOverlayProps { * Visibility follows the same rule as the rest of Ontime's floating chrome: it * fades once the operator stops moving, so it leaves the talent's eyeline * without needing to know whether the script happens to be rolling. + * + * Every control is present for the life of the view and goes disabled when it + * has nothing to do. A control which comes and goes moves every control after + * it, so the operator reaches for pause during a show and presses whatever slid + * under their finger instead. */ export default function ControlOverlay({ isRunning, speed, - followLocked, + canReengageFollow, atEnd, controller, onToggleHelp, @@ -50,47 +57,67 @@ export default function ControlOverlay({ return (
- + } > {isRunning ? : } - + - controller.changeSpeed(-SPEED_STEP))} - aria-label='Slow down' + controller.changeSpeed(-SPEED_STEP))} + aria-label='Slow down' + /> + } > - +
{speed} lpm
- controller.changeSpeed(SPEED_STEP))} - aria-label='Speed up' + controller.changeSpeed(SPEED_STEP))} + aria-label='Speed up' + /> + } > - + - controller.rewind())} - aria-label='Rewind to top' + controller.rewind())} + aria-label='Rewind to top' + /> + } > - + {/* The operator view solves this with its own FollowButton, a labelled pill @@ -98,21 +125,37 @@ export default function ControlOverlay({ two would land on top of each other. Same icon and same job, folded into the transport rather than floating separately. */} - {followLocked && ( - - - - )} + + } + > + + - + + } + > - +
); } diff --git a/e2e/tests/features/215-teleprompter.spec.ts b/e2e/tests/features/215-teleprompter.spec.ts index 73a877e25..e612f223c 100644 --- a/e2e/tests/features/215-teleprompter.spec.ts +++ b/e2e/tests/features/215-teleprompter.spec.ts @@ -116,6 +116,27 @@ test.describe('teleprompter', () => { expect(slower).toBe(before); }); + test('keeps every transport control in place', async ({ page }) => { + // a control which appears mid show moves the ones after it, and the operator + // reaches for pause and presses whatever slid under their finger instead + await page.goto(teleprompterUrl); + await expect(scroller(page)).toBeVisible(); + + const controls = page.locator('.teleprompter__controls button'); + const follow = page.getByTestId('teleprompter-follow'); + + await expect(controls).toHaveCount(6); + await expect(follow).toBeVisible(); + await expect(follow).toBeDisabled(); + + // taking over by hand offers the follow back, without restacking the row + await page.mouse.move(640, 300); + await page.mouse.wheel(0, 200); + + await expect(controls).toHaveCount(6); + await expect(follow).toBeVisible(); + }); + test('f mirrors the view for a beam splitter rig', async ({ page }) => { await page.goto(teleprompterUrl); await expect(scroller(page)).toBeVisible();