Commit Graph

1575 Commits

Author SHA1 Message Date
Claude 583ece764d refactor(teleprompter): share the shortcut list, drop the dim option
The help dialog had been laid out to match the rundown editor's shortcut
list by copying its structure and its stylesheet. The pieces both were built
from now live together, so the two lists cannot drift: the editor keeps its
palette, the viewer keeps its own, and everything else is shared.

Dimming what has already been read is no longer a setting. Prompter software
treats the cue marker and the shading as two halves of one reading guide, and
this was a switch with no reason to be touched. Removing it takes the option,
its parse, its type and the prop with it.

The reading line moves to a quarter from the top. It leaves more of the
script visible below the line, which is the part being read towards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-19 11:12:43 +00:00
Claude 15c898259f Revert "refactor(views): share the hook which reads view options"
This reverts commit b4e3e0e553.
2026-08-19 05:02:57 +00:00
Claude b4e3e0e553 refactor(views): share the hook which reads view options
Six views carried a character identical hook to read their options from the
query and let a preset override them. The teleprompter added a seventh copy,
and then a second statement of the same precedence rule in the param mirror,
which has to know that a value a preset pins cannot be written back.

The rule now has one home and one name. Each view is left with the part which
is actually its own, its parser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-19 04:23:32 +00:00
Claude 52f91357a2 refactor(teleprompter): enforce each invariant in one place
A maintenance pass over the view. Nothing changes on screen.

The position was bounded twice on the playing path: advance() clamped its
result and the loop clamped it again. The loop has to clamp regardless, for
nudges and for a document which shrank, so advance() now reports reaching
the end and leaves the bound to its only caller.

maxScroll was floored at zero at each of four reads, though measure() is its
only writer and already floors it. The invariant is stated once, where it is
established.

Arrival at an eased target was tested twice: easeCatchUp snaps to the target
once it is close enough, and hasArrived then measured the same distance
again. Equality with the target is the signal, so hasArrived is gone.

Speed was clamped to its range by the option parser and then clamped to the
identical range again by clampSpeed, on the same call. Only the keys and
buttons, which add deltas, still need to clamp.

The Enter guard in the key handler could never fire: the keymap does not bind
Enter, so the event was already passing through untouched. The reason Enter
stays unbound now lives in the keymap, with a test, rather than as a branch
which reads as though something was being prevented.

Also drops a redundant early return in ReadingLine, an export with no callers
outside its module, and a JSON round trip in the param mirror which existed
only to satisfy a dependency array.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-18 19:00:12 +00:00
Claude 8ec12ae4d2 feat(teleprompter): record the live controls in the url
Speed, font size and the flips could each be set from two places: the view
params editor, which writes the query, and the prompter's own keys and
buttons, which wrote React state and nothing else. The URL therefore
described how the view was opened rather than how it had been tuned, so a
link copied after setting the prompter up handed the next person a
different prompter, and a reload threw the setup away.

The live controls now mirror into the same query the params editor writes,
so the existing features built on it — sharing a link, saving a URL preset,
redirecting a client — describe what is actually on screen without changes
of their own. Settings left at their default stay out of the query, so a
shared link carries only what was deliberately changed.

The write is debounced and replaces rather than pushes: the arrow keys
repeat while held, and neither a held key nor a foot pedal should fill the
address bar or the back button with one entry per frame.

Font size was a hidden multiplier on top of the option, which had no way to
be expressed as a param. It is now the size itself, stepped by a ratio so a
press is the same visual change at any size, sharing the option's own range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-18 18:42:01 +00:00
Claude bb8f147575 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-18 18:24:05 +00:00
Claude 6ef04d9a92 feat(teleprompter): let the cue arrow use the margin it has
The text width option leaves a wide gutter which the indicator was barely
touching. It is now sized against that gutter rather than against the
viewport, so it fills a useful part of the space beside the line: roughly
three times the area at the default width, and it grows with the font.

Sizing it from the gutter also fixes a case the fixed width did not handle.
A column set to the full width leaves nowhere for the arrow to go, and it
now collapses instead of being drawn over the first word.

Prompted by a look at how Ikan's PrompterPro presents the same control,
where the eyeline indicator is a large arrow sitting clear of the script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-18 12:26:39 +00:00
Claude 1d78090e19 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
2026-08-18 12:17:48 +00:00
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 8253f1c119 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
2026-08-16 19:46:30 +00:00
Claude 194fe0fb16 refactor(teleprompter): collapse what the option cuts left behind
The scroll speed was held three times: as state for the readout, as a ref the
loop can read, and as a cached pixel rate. The cached rate was written from two
places, the measure pass and the speed effect, so either one forgetting would
have left the script scrolling at a stale rate. It is a multiplication of two
values the loop already has, so the loop now does it per frame and the cache
and one of its writers are gone.

The select values and the values the parser accepts were written out twice.
Adding a value to a select would have left the parser rejecting it and falling
back to the default, with the view simply ignoring the choice. The accepted
values are now derived from the offered ones, and a test walks every value in
every select through the parser. Confirmed the test fails when the old
duplication is put back.

Also removed a satisfies clause that constrained nothing, since every object
satisfies Partial<Record<string, unknown>>, and typed the two enum defaults so
the parser no longer casts its fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-16 09:37:50 +00:00
Claude 1f617bed56 refactor(teleprompter): drop the font and colour options
These were justified as matching what the other views offer, which was wrong.
Only the timer declares a font or a key colour, and no view at all declares a
text colour, so this was the most customisable view in Ontime rather than one
following a convention.

On their own merits none of the three holds up. A prompter wants the highest
contrast it can get, which is what it now always renders: white on black in the
default font. Key colour is a chroma key idea that means nothing on a screen
read by a person standing in front of it.

The escape hatch already exists and is better than two colour pickers. This
view is wrapped in ViewLoader, which injects the project's CSS override
stylesheet, so anyone needing another palette, an inverted display for bright
conditions, or a particular typeface can style all of it.

Checked that a saved preset naming these params is simply ignored rather than
breaking, since presets outlive the options that made them.

Fourteen options remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-16 09:31:00 +00:00
Claude b42b10829a refactor(teleprompter): drop the two options that cannot justify themselves yet
Options are permanent surface. They live in URLs and in saved presets, so one
can be added later without breaking anything while removing one breaks every
preset that named it. That asymmetry says to keep only what earns its place
before the view ships, not after.

Autoplay goes. Its own code said as much: it was the single option needing a
lint suppression because it is a starting condition rather than a live setting,
which is not how any other option here behaves. It also has no coherent use.
Starting a scroll on load runs the script at a fixed rate whether or not
anybody is speaking, and following the loaded event, which is on by default,
immediately pulls the position somewhere else. Anyone wanting a prompter to
roll unattended needs the transport control we have not built.

Alignment goes. Nothing in the research pointed at it as a control operators
reach for, and centring multi line paragraphs leaves a ragged left edge which
is harder to track back to on each new line. Left is the safer default and the
option can come back cheaply if somebody asks for it.

Seventeen options remain, and each maps to something the research called for or
to the font and colour trio the other views already share.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-16 09:04:34 +00:00
Claude 509305ee61 refactor(teleprompter): follow React on state sync and stop touching the DOM idly
Three findings from a pass over performance and React practice.

Two options were synced from their params in effects, which is the pattern the
React docs warn against. It is not only style here: an effect runs after the
commit, so the view painted a frame in the previous orientation before
correcting itself, and on a prompter that reads as a flash. Both the flip pair
and the live speed are now adjusted during render, which React discards before
it reaches the screen. The flip state also collapses from two booleans and two
effects into one object.

The keyboard handler assigned its arguments to a ref during render so the
listener could stay installed once. That is a side effect in a place React may
run twice or throw away, so it moved to an effect.

The animation frame loop wrote scrollTop on every frame even when nothing had
moved, so a paused prompter kept poking the DOM sixty times a second. It now
writes only when the position changed.

Measured rather than assumed, against a 200 event script, 214 blocks and a
198000 pixel document: 16.7ms median frames with no long tasks, both paused and
running. A speed change costs a single frame at 29ms for the re-render. The
worry about long rundowns needing virtualisation does not hold at this size.

Confirmed the view still compiles under the React compiler after the render
phase updates, at 51 memo caches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-15 14:34:08 +00:00
Claude 37fab6a12d refactor(teleprompter): remove duplicated knowledge the compiler cannot police
A maintenance pass over the view, aimed at the places where a later change
would go wrong quietly rather than fail.

Every option default existed three times: the declaration the params editor
renders, the fallback the parser applies, and a fallback in the stylesheet that
nothing could reach because the view always sets the variable. Editing one left
the editor showing a value the view was not using. Defaults and bounds now live
in one object which both the declaration and the parser read, and the dead
stylesheet copies are gone. A test asserts that parsing an empty query returns
exactly what the editor declares, so the two cannot drift apart again.

That test found the first case immediately: the script source declared a
default of none but parsed to null. It is now none in both, which also takes a
null out of the options type and a branch out of the view.

The speed step was a constant in the keymap and a literal in the overlay, so
the buttons would have kept stepping by two if the constant ever changed. Both
now read the same export.

The line height was measured by looking up an element by a data attribute set
in another file. Removing that attribute would not have failed, it would have
fallen back to a guess which happens to be correct at the default line height
and wrong at any other. The measurement now reads the element the hook already
holds, which is where the stylesheet sets the line height. Confirmed against
the browser: at double the line height the script travels 1.98 times as far.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-15 09:46:07 +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 8233261606 fix(teleprompter): apply the flip params on change, not only on mount
The flip options seeded useState and were then never read again, so a change
took effect only on a fresh load. Toggling flip in the view options did
nothing until the page was reloaded.

They now sync when the param changes, the way the speed option already does,
while the F key still owns the state between changes.

This also matters beyond the params editor. Ontime can redirect a specific
client to a path, and the teleprompter keeps its whole configuration in the
query string, so one device can already reconfigure a prompter running on
another. Verified that a same route query change applies live and leaves the
scroll position untouched, which is what makes that usable during a show.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-14 17:32:16 +00:00
Claude 23a184244c refactor(teleprompter): let the compiler memoise the view
The client runs babel-plugin-react-compiler in annotation mode, so 'use memo'
is the opt in and the codebase already uses it in eighteen places. The
teleprompter was the odd one out, hand rolling what the compiler does better.

Teleprompter and ScriptBlock now carry the directive, which replaces two
useMemo calls, four useCallback calls and a memo wrapper. Verified against the
compiler rather than assumed: the two files emit 46 and 15 memo cache slots
respectively, finer grained than the six the hand written version had.

The scroll engine and the key handler deliberately keep no directive. They
derive nothing and render nothing, they drive an animation frame loop through
refs, so there is nothing to memoise and no reason to put them through the
compiler.

Also fixes a hazard the conversion surfaced. The follow effect listed the
blocks array in its dependencies, but memoisation is a performance hint React
is free to discard, under the compiler as much as by hand. A rebuilt but
otherwise identical list would re-fire the jump to the loaded event and yank
the script back mid read. It now watches whether the loaded event has a block
at all, which is the condition it actually cared about and does not depend on
identity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-13 19:19:20 +00:00
Claude e9289685fd refactor(navigation): suppress the Space hotkey by prop
Replaces the view hotkey store with a suppressSpaceHotkey prop on
ViewNavigationMenu, alongside the suppressSettings prop which already exists
for exactly this purpose: a view opting out of a navigation chrome behaviour.

The store had one advantage, that it covered the preset route without the
router naming the view. That is a single expression in PresetView, and paying
for it with a global claim was the wrong trade: the claim was lifecycle state
that had to be released on unmount to avoid stranding Space for the whole
client, where a prop simply describes the route.

Verified both routes: /teleprompter and a locked preset targeting the
teleprompter leave Space to the view, while the timer and a preset targeting
the timer still open the navigation menu with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-13 19:13:02 +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 15dac35748 feat(teleprompter): honour the shared Flip Screen toggle
The teleprompter was the only view ignoring the global Flip Screen option,
so it now composes with the per view flips.

The two turn out to be the same mechanism: the shared `.mirror` class is
rotate(180deg), which is the same matrix as scale(-1, -1), so Flip Screen
is exactly a flip on both axes at once. Folding it in with XOR means the
teleprompter matches every other view when Flip Screen is on, and there is
still only one transform on the element.

It cannot replace the per view flips. A rotation preserves handedness, so
it never produces the mirror image a beam splitter reflection needs, and it
cannot address one axis on its own. Those remain URL params, which also
lets a shared link to the talent screen carry the rig's setup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-13 05:25:44 +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
Carlos Valente c6eccec30e refactor(settings): show new app indicator 2026-08-09 16:48:20 +02:00
Carlos Valente 5220c2c374 fix(settings): prevent loader overflow 2026-08-09 16:48:20 +02:00
Carlos Valente 4eeeb294f7 chore: update electron navigation 2026-08-09 16:48:20 +02:00
Alex Christoffer Rasmussen a006331fea Group duration context menu utils (#1748) 2026-08-09 16:45:47 +02:00
Carlos Valente ac0ef06459 bump version to 4.12.0 2026-08-09 10:44:13 +02:00
Carlos Valente fb559ed9da refactor(progress bar): reset when event changes 2026-08-05 15:30:22 +02:00
Carlos Valente 24be49ef66 refactor(settings): polish settings UI 2026-08-02 13:44:28 +02:00
Carlos Valente f0c284b708 refactor(link): add row presentation variant 2026-08-02 13:44:28 +02:00
Carlos Valente 9e42f18299 feat(settings): add searchable navigation 2026-08-02 13:44:28 +02:00
Carlos Valente bdd815678b refactor(ui): extend link and modal variants 2026-08-02 13:44:28 +02:00
Carlos Valente 12fc76b8ff refactor(op): simplify UI elements 2026-08-02 12:55:49 +02:00
Carlos Valente 8ed0a1689e refactor(op): clarify settings text 2026-08-02 12:55:49 +02:00
Carlos Valente ba18b8dba7 refactor(ui): rundown title UI polish 2026-08-02 12:55:49 +02:00
Carlos Valente 608247157b refactor(rundown): cap rundown names to 64 characters 2026-08-02 12:55:49 +02:00
Carlos Valente 514b6c3a02 refactor(navigation): custom params indicators only for current view 2026-08-02 12:55:49 +02:00
Carlos Valente 2050735014 refactor(ui): title alignment is logo aware 2026-08-02 12:55:49 +02:00
Carlos Valente 76b1341432 refactor(logo): upload on submit form 2026-08-02 10:39:58 +02:00
Carlos Valente c7422fc7c7 refactor(logo): improve flow for managing logo 2026-08-02 10:39:58 +02:00
Carlos Valente 94d54529ee refactor(views): consistent loading and empty states 2026-08-01 16:25:14 +02:00
Carlos Valente f7535651f6 refactor(info): improve UI consistency and polish 2026-08-01 14:41:39 +02:00
Carlos Valente 5cf36f049a refactor(modal): allow wider modals 2026-07-25 14:28:45 +02:00
Carlos Valente c6248b0c72 refactor(import): improve preview UI 2026-07-25 14:28:45 +02:00
Alex Christoffer Rasmussen 2a890cf2b3 fix: issue where a countToEnd would lead to incorrect expected times (#2149)
* fix: issue where a count-to-end would lead to incorrect expected times

* fix: include add time in overtime when countToEnd

* fix: ui and server use same calculation for expected end
2026-07-25 11:22:24 +02:00
Carlos Valente a8c611911d refactor(rundown): new entries are appended to rundown 2026-07-22 07:17:39 +02:00
Carlos Valente 8363f06a5c chore(rundown): improve documentation of insert logic 2026-07-22 07:17:39 +02:00
will@t1d.dev 5f040092cb fix: timer-legacy view not served in desktop distributions
The server serves html/timer-legacy.html and html/login.html from disk
at runtime, relative to the bundled server. The Docker image copies
these files but the electron packaging did not, so the view 404ed in
all desktop distributions.

Additionally, AppImages mount at /tmp/.mount_*, a hidden directory.
Express sendFile refuses paths containing dot-segments by default
(returns 404 without touching disk), so the view failed on Linux even
with the file packaged. Allow dotfiles for this route; the request
path is fixed so no user input is affected.
2026-07-19 10:02:45 +02:00
Carlos Valente 347c748dd9 feat(rundown): add shortcut to jump to current element 2026-07-18 17:03:02 +02:00