mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 21:03:29 +00:00
refactor(teleprompter): build the control overlay from the shared pieces
The overlay is the on screen transport, for whoever is not at a keyboard, which on a tablet prompter is everyone. It was written from scratch and three things it needed already existed. Its buttons are now IconButton in the subtle-white variant, the same as the floating navigation uses over a dark viewer, which removes a block of bespoke button styling that had reinvented sizing, centring, radius and hover. The help overlay's close button likewise uses the shared Button. Visibility now comes from useFadeOutOnInactivity, the hook the floating navigation uses, so the overlay fades once the operator stops moving. It used to fade on whether the script was rolling, which meant a paused prompter kept a control bar in the talent's eyeline indefinitely. Following the shared pattern turned up a bug in the old one. The floating navigation stops taking clicks once faded, and this did not, so there was an invisible transport across the foot of the script where a stray tap would pause the read. It now matches. The operator view's FollowButton is deliberately not reused. It is a labelled pill fixed to the bottom centre, which is exactly where this group sits, so the two would land on top of each other. Same icon and same job, folded into the transport instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
This commit is contained in:
@@ -178,35 +178,15 @@
|
||||
opacity: 1;
|
||||
transition: opacity $viewer-transition-time;
|
||||
|
||||
/* stay out of the eyeline while the script is rolling, until pointed at */
|
||||
/**
|
||||
* useFadeOutOnInactivity drives this, as it does the floating navigation, and
|
||||
* the same rule applies: once faded the controls stop taking clicks. Leaving
|
||||
* them live would put an invisible transport across the foot of the script,
|
||||
* where a stray tap pauses the read.
|
||||
*/
|
||||
&--idle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.teleprompter__control {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
font-size: clamp(14px, 1.4vw, 22px);
|
||||
|
||||
color: $viewer-color;
|
||||
background: rgba(white, 8%);
|
||||
border-radius: $element-border-radius;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(white, 16%);
|
||||
}
|
||||
|
||||
&--attention {
|
||||
color: $accent-color;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,9 +250,4 @@
|
||||
|
||||
.teleprompter__help-close {
|
||||
margin-top: $view-element-gap;
|
||||
padding: 0.4em 1em;
|
||||
color: $viewer-color;
|
||||
background: rgba(white, 8%);
|
||||
border-radius: $element-border-radius;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IoArrowUp, IoHelpCircleOutline, IoLocate, IoPause, IoPlay, IoRemove, IoAdd } from 'react-icons/io5';
|
||||
import { IoAdd, IoArrowUp, IoHelpCircleOutline, IoLocate, IoPause, IoPlay, IoRemove } from 'react-icons/io5';
|
||||
|
||||
import IconButton from '../../../common/components/buttons/IconButton';
|
||||
import { useFadeOutOnInactivity } from '../../../common/hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { SPEED_STEP } from '../teleprompter.scroll';
|
||||
import type { TeleprompterController } from '../teleprompter.types';
|
||||
@@ -14,8 +16,12 @@ interface ControlOverlayProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* On screen transport, for when nobody is at a keyboard.
|
||||
* It fades out while the script is rolling so it does not sit in the talent's eyeline.
|
||||
* Transport for whoever is not at a keyboard, which on a tablet prompter is
|
||||
* everyone. The keys remain the primary interface, and the way pedals reach it.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export default function ControlOverlay({
|
||||
isRunning,
|
||||
@@ -25,65 +31,73 @@ export default function ControlOverlay({
|
||||
controller,
|
||||
onToggleHelp,
|
||||
}: ControlOverlayProps) {
|
||||
const isActive = useFadeOutOnInactivity(true);
|
||||
|
||||
return (
|
||||
<div className={cx(['teleprompter__controls', isRunning && 'teleprompter__controls--idle'])}>
|
||||
<button
|
||||
type='button'
|
||||
className='teleprompter__control'
|
||||
<div className={cx(['teleprompter__controls', !isActive && 'teleprompter__controls--idle'])}>
|
||||
<IconButton
|
||||
variant='subtle-white'
|
||||
size='large'
|
||||
onClick={controller.togglePlay}
|
||||
data-testid='teleprompter-play'
|
||||
aria-label={isRunning ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isRunning ? <IoPause /> : <IoPlay />}
|
||||
</button>
|
||||
</IconButton>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
className='teleprompter__control'
|
||||
<IconButton
|
||||
variant='subtle-white'
|
||||
size='large'
|
||||
onClick={() => controller.changeSpeed(-SPEED_STEP)}
|
||||
aria-label='Slow down'
|
||||
>
|
||||
<IoRemove />
|
||||
</button>
|
||||
</IconButton>
|
||||
|
||||
<div className='teleprompter__speed' data-testid='teleprompter-speed'>
|
||||
{speed}
|
||||
<span className='teleprompter__speed-unit'>lpm</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
className='teleprompter__control'
|
||||
<IconButton
|
||||
variant='subtle-white'
|
||||
size='large'
|
||||
onClick={() => controller.changeSpeed(SPEED_STEP)}
|
||||
aria-label='Speed up'
|
||||
>
|
||||
<IoAdd />
|
||||
</button>
|
||||
</IconButton>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
className={cx(['teleprompter__control', atEnd && 'teleprompter__control--attention'])}
|
||||
<IconButton
|
||||
variant={atEnd ? 'primary' : 'subtle-white'}
|
||||
size='large'
|
||||
onClick={() => controller.rewind()}
|
||||
aria-label='Rewind to top'
|
||||
>
|
||||
<IoArrowUp />
|
||||
</button>
|
||||
</IconButton>
|
||||
|
||||
{/*
|
||||
The operator view solves this with its own FollowButton, a labelled pill
|
||||
fixed to the bottom centre. That is exactly where this group sits, so the
|
||||
two would land on top of each other. Same icon and same job, folded into
|
||||
the transport rather than floating separately.
|
||||
*/}
|
||||
{followLocked && (
|
||||
<button
|
||||
type='button'
|
||||
className='teleprompter__control teleprompter__control--attention'
|
||||
<IconButton
|
||||
variant='primary'
|
||||
size='large'
|
||||
onClick={controller.reengageFollow}
|
||||
data-testid='teleprompter-follow'
|
||||
aria-label='Follow the loaded event'
|
||||
>
|
||||
<IoLocate />
|
||||
</button>
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<button type='button' className='teleprompter__control' onClick={onToggleHelp} aria-label='Keyboard shortcuts'>
|
||||
<IconButton variant='subtle-white' size='large' onClick={onToggleHelp} aria-label='Keyboard shortcuts'>
|
||||
<IoHelpCircleOutline />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Button from '../../../common/components/buttons/Button';
|
||||
|
||||
interface HelpOverlayProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
@@ -37,9 +39,9 @@ export default function HelpOverlay({ onClose }: HelpOverlayProps) {
|
||||
<div className='teleprompter__help-note'>
|
||||
Foot pedals and hand controllers which emit these keys work without any setup.
|
||||
</div>
|
||||
<button type='button' className='teleprompter__help-close' onClick={onClose}>
|
||||
<Button variant='subtle-white' onClick={onClose} className='teleprompter__help-close'>
|
||||
Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user