Commit Graph

5 Commits

Author SHA1 Message Date
Claude 6589bcceee fix(views): make boolean view params work, and retune the prompter
The params editor could not represent a boolean which defaults to true.
Two bugs stacked:

- ParamInput fell back with `isStringBoolean(param) ?? defaultValue`, but
  isStringBoolean answers false for an absent param rather than nothing, so
  the ?? never fired and every switch opened off whatever its option said.
- An unchecked checkbox is absent from the form data rather than present and
  false, so switching one off wrote no param and the parser fell back to the
  default the user was trying to leave.

Either one alone is invisible while every boolean defaults to false, which is
why this surfaced with the teleprompter. Together they made the whole panel
look inert: the switch showed off, the view showed on, and Apply did nothing.

Prompter changes from the review:

- Speed is calibrated against the reading rate rather than picked for feel.
  30 lines per minute was about 300 words per minute, roughly twice a
  broadcast read; the default is now 12, measured at 129 wpm on the default
  column. The ceiling comes down from 200 to 40 so the arrows stay useful.
- Smaller, denser defaults: 40px over 1.3 line height in an 80% column, which
  is 21 lines on a 1080p screen where the old defaults gave 11.
- The reading line is a marker one line tall in the gutter beside the text,
  replacing the rule across the words and the pair of margin arrows. The
  option is a boolean now that there is one style rather than three.
- Space always drives the transport. It was deferring to whichever control
  had focus, so a prompter stopped responding to the pedal after anyone
  touched a button; the overlay drops focus after a pointer press instead.
  Enter still activates a focused control, so the overlay stays keyboard
  operable.
- The help dialog is laid out as the rundown shortcuts panel is, down to the
  Kbd keycaps and the grouping, and no longer explains foot pedals.

Headings stay on the same left rail as the script rather than centred: they
are signposts for the operator, and a second alignment would give the eye
something new to find at every segment change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-18 12:00:25 +00:00
Claude 8a31cc5018 fix(teleprompter): address review findings and the e2e ordering failure
The e2e run failed because 214-rundown-switch-edit loads a fresh rundown,
leaving the teleprompter spec with nothing to read. The spec now seeds its
own script entry, idempotently, so it no longer depends on which rundown a
previous spec happened to leave loaded.

Review findings:

- PresetView rendered outside ViewLoader, so a view reached through a preset
  never got the project's CSS override stylesheet.
- The help overlay was a bare div. It is now a Dialog, so focus moves into it
  and back out, Escape closes it instead of rewinding the script, and the
  prompter keymap stands down while it is open.
- Space is left unbound rather than made a no-op when a view claims it.
  useHotkeys calls preventDefault before reaching the handler, so an early
  return still swallowed the key and stopped Space activating a focused button.
- The scroll observer attached on mount only, so a scroller which mounted later
  (the empty state resolving into a script) was never measured and could not
  play. Callback refs attach it whenever the elements appear.
- jumpToEnd marked the end state before the document had been measured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-17 19:04:32 +00:00
Claude 00b3ba8ed3 refactor(teleprompter): simplify the scroll engine and drop settings that overlap
Three simplifications from a review of where the complexity actually sat.

The animation frame loop now runs for the lifetime of the view instead of being
woken around each action. Waking a sleeping loop meant that every path which
could move the script had to remember to do it, across eight call sites, and
forgetting simply lost the action with no error. An idle frame costs one pass
of arithmetic and the browser suspends the loop entirely while the tab is
hidden. This removes ensureLoop, the frame handle and the work test.

Running always did break something, which is worth recording: any scroll the
loop had not made itself was reverted on the next frame, so a scrollbar drag or
find in page would snap back. The loop now compares the scroller against where
it left it and adopts the DOM position when they differ. That covers every
cause rather than only the gestures which had a handler, so the adopt flag and
its wiring are gone too, and there is an end to end test for it.

Dropped hidePast in favour of dimPast. They read as alternatives but only one
is safe during a read: removing past events shortens the document underneath
the reader, while dimming is only paint. Also dropped the shade reading line
variant, which drew the same gradient over the same box as dimPast, so turning
both on stacked two of them.

The reading line position was carried as two CSS variables, a percentage and a
unitless number. That split caused the padding bug earlier. One unitless
variable now serves both, scaled by 1% for the overlays and 1dvh for the
padding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-14 19:17:16 +00:00
Claude 323b30ea68 refactor(teleprompter): drop code that was not earning its keep
Review pass over the view.

Removed: estimateWordsPerLine and linesPerMinuteToWordsPerMinute, written for
a words-per-minute readout that was never built; play and pause on the
controller, which nothing outside the hook called; and three constants that
were exported but never imported.

The tick function was being reassigned to a ref on every render, which is a
side effect during render. It only ever touched refs and state setters, so it
is now a stable callback and the ref is gone.

The synthetic contentKey string is replaced by the memoised blocks array it
was standing in for. It was also a dependency of the ResizeObserver effect,
which tore the observer down and rebuilt it for no gain: the observer already
covers every reflow that changes the document.

ScriptBlock was memoised but never actually memoising, because the parent
built its ref callback inline and handed it a new identity every render. That
also churned the follow map, unregistering and re-registering every block. The
id is now bound inside the block against a stable callback.

Tests: dropped six that asserted arithmetic identities or wrapped clamps
rather than behaviour, and added three for branches that were untested,
group titles across and back into a group, and a heading with no cue.
The e2e rewind assertion waited 600ms for an eased scroll that needs about
900ms from a nudge and a second from the bottom of a long script; it now
polls. The navigation menu assertion raced app hydration and now waits for
the view, as the existing navigation tests do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-13 16:42:07 +00:00
Claude f806244586 feat(teleprompter): add teleprompter view
Adds a teleprompter at /teleprompter which builds its script from the
rundown rather than from an uploaded file, so the read follows the show.

The script comes from a text custom field chosen per view, and the whole
rundown renders as one continuous document with a heading per segment.
That is how broadcast prompters work: the operator scrolls to the right
section as the show moves, so a hard cut on every event change would take
the tail of the line the talent is still reading. Following the loaded
event is a soft jump which releases when the user scrolls by hand, the
same interaction the operator view already uses.

Controls are local, and match the convention shared by prompter software:
space to run, arrows for speed and nudge, home to rewind, F to flip. That
convention doubles as the hardware protocol, since foot pedals and hand
controllers are USB HID devices emitting these keystrokes, so they work
with no setup. Space is claimed back from the navigation menu for the
lifetime of the view via a small store, since the router renders the menu
generically for presets and a prop would not reach it.

Scrolling uses native scrollTop on an overflow container, with the
animation frame loop as its only writer. Position is kept as a float in a
ref: at readable speeds the per frame movement is well under a pixel, so
rounding every frame would stall the scroll, and holding it in state would
re-render the document sixty times a second. Scroll anchoring and smooth
scroll behaviour are both disabled because each would be a second writer.

Notable details:
- flip applies to the view root, so a beam splitter inverts the scroll
  direction along with the text
- content padding is derived from the viewport height, not a percentage,
  which resolves against width and would strand the first line
- image custom fields are refused even when typed into the URL
- script text is rendered as a text node, never as markup

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-13 05:20:46 +00:00