fix(teleprompter): larger text, a cue triangle and a corner close

Follow-up to the review pass:

- 40px was too small once it was on a screen. 52px over the 1.3 line height
  keeps most of the density win, at 16 lines on a 1080p screen against the 11
  the original defaults gave.
- The speed follows the text size, since a larger font puts fewer words on a
  line. 14 lines per minute measures at 136 wpm on the new defaults, where 12
  had dropped to 116.
- The reading indicator is a triangle pointing in at the line rather than a
  bar. It is the shape prompter cue indicators have settled on: it names the
  line from the margin without putting a mark near the words.
- The help dialog closes from an x in its corner, as the navigation menu and
  the params editor already do, rather than from a button under the list.
- Teleprompter sits above Project Info in the navigation menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
This commit is contained in:
Claude
2026-08-18 12:17:48 +00:00
parent 6589bcceee
commit 1d78090e19
6 changed files with 36 additions and 23 deletions
+1 -1
View File
@@ -4,8 +4,8 @@ export const navigatorConstants = [
{ url: 'timeline', label: 'Timeline' },
{ url: 'studio', label: 'Studio Clock' },
{ url: 'countdown', label: 'Countdown' },
{ url: 'info', label: 'Project Info' },
{ url: 'teleprompter', label: 'Teleprompter' },
{ url: 'info', label: 'Project Info' },
];
// default time format to use for users in 12 hour clocks
@@ -151,15 +151,22 @@
pointer-events: none;
}
/**
* A triangle pointing in at the line, which is the shape prompter cue
* indicators have settled on: it names one line without touching the words,
* and it reads as a pointer from the edge rather than as punctuation in the
* script. Clipped from a filled box rather than built from borders so its
* height stays tied to the parent's single line.
*/
.teleprompter__reading-marker {
position: absolute;
top: 0;
bottom: 0;
/* sits in the gutter beside the column, never over the words */
right: calc(100% + 0.4em);
width: 0.16em;
border-radius: 0.08em;
right: calc(100% + 0.35em);
width: 0.45em;
background: $accent-color;
clip-path: polygon(0 0, 100% 50%, 0 100%);
font-size: var(--tp-font-size);
}
@@ -238,10 +245,17 @@
font-size: 1rem;
}
.teleprompter__help-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.25rem;
}
.teleprompter__help-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1.25rem;
}
.teleprompter__help-groups {
@@ -297,7 +311,3 @@
color: $viewer-label-color;
font-size: calc(1rem - 5px);
}
.teleprompter__help-close {
margin-top: 1.5rem;
}
@@ -12,7 +12,7 @@ describe('getOptionsFromParams()', () => {
showGroups: true,
speed: DEFAULT_SPEED,
followLoaded: true,
fontSize: 40,
fontSize: 52,
lineHeight: 1.3,
textWidth: 80,
dimPast: true,
@@ -60,7 +60,7 @@ describe('getOptionsFromParams()', () => {
// Number(null) is 0, so a naive parse would silently produce a speed of zero
expect(getOptionsFromParams(new URLSearchParams('speed=fast')).speed).toBe(DEFAULT_SPEED);
expect(getOptionsFromParams(new URLSearchParams('speed=')).speed).toBe(DEFAULT_SPEED);
expect(getOptionsFromParams(new URLSearchParams('fontSize=huge')).fontSize).toBe(40);
expect(getOptionsFromParams(new URLSearchParams('fontSize=huge')).fontSize).toBe(52);
});
test('rejects an unknown value for an enumerated option', () => {
@@ -1,7 +1,8 @@
import { Dialog } from '@base-ui/react/dialog';
import type { PropsWithChildren } from 'react';
import { IoClose } from 'react-icons/io5';
import Button from '../../../common/components/buttons/Button';
import IconButton from '../../../common/components/buttons/IconButton';
import Kbd from '../../../common/components/kbd/Kbd';
interface HelpOverlayProps {
@@ -32,7 +33,12 @@ export default function HelpOverlay({ isOpen, onClose }: HelpOverlayProps) {
<Dialog.Portal>
<Dialog.Backdrop className='teleprompter__help' />
<Dialog.Popup className='teleprompter__help-card'>
<Dialog.Title className='teleprompter__help-title'>Prompter shortcuts</Dialog.Title>
<div className='teleprompter__help-header'>
<Dialog.Title className='teleprompter__help-title'>Prompter shortcuts</Dialog.Title>
<IconButton variant='subtle-white' size='large' onClick={onClose} aria-label='Close'>
<IoClose />
</IconButton>
</div>
<div className='teleprompter__help-groups'>
<ShortcutGroup title='Transport'>
@@ -94,10 +100,6 @@ export default function HelpOverlay({ isOpen, onClose }: HelpOverlayProps) {
</Shortcut>
</ShortcutGroup>
</div>
<Button variant='subtle-white' onClick={onClose} className='teleprompter__help-close'>
Close
</Button>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
@@ -38,7 +38,7 @@ const defaults = {
showGroups: true,
speed: DEFAULT_SPEED,
followLoaded: true,
fontSize: 40,
fontSize: 52,
lineHeight: 1.3,
textWidth: 80,
readingLine: true,
@@ -10,14 +10,15 @@
*
* The default is calibrated against the reading rate rather than picked for
* feel: broadcast presenters read at 140-160 words per minute and conference
* talent slower still, and at the default column width a line carries a dozen
* or so words. Twelve lines per minute lands in that band. The ceiling is set
* where the text stops being readable at all, not at the fastest the loop can
* physically scroll, so the arrow keys stay useful across their whole range.
* talent slower still, and at the default size a line carries around ten words.
* Fourteen lines per minute measures at 136 wpm, a presenter's pace rather than
* a news reader's. The ceiling is set where the text stops being readable at
* all, not at the fastest the loop can physically scroll, so the arrow keys
* stay useful across their whole range.
*/
export const MIN_SPEED = 1;
export const MAX_SPEED = 40;
export const DEFAULT_SPEED = 12;
export const DEFAULT_SPEED = 14;
/** how much one speed adjustment moves, shared by the keymap and the overlay */
export const SPEED_STEP = 1;