Files
ProdDash/modules/propresenter-timers/style.css
T
Waters ChurchandClaude Fable 5 cb77030eec Timers: LTC from the audio signal itself via an external reader
ProPresenter 21.4 exposes no timecode through any API, so read the LTC
where it actually lives — the audio feed:

- tools/ltc-reader.js: zero-dependency SMPTE LTC decoder (biphase-mark,
  adaptive bit clock, envelope-scaled comparator) fed s16le PCM on stdin
  from ffmpeg/sox; POSTs running/stopped/nosignal + timecode + frame rate
  (incl. 29.97 drop-frame) to the module. --selftest runs an encoder→
  decoder suite (24/25/30 fps, DF minute counting, low level + noise + DC,
  dropout recovery); --demo synthesizes live LTC through the real decoder
  and ingest path with no audio hardware.
- Module: POST /ltc ingest route (optional bearer token, validated body,
  6 s staleness with automatic fallback to the API probe / stage source),
  richer LTC state (status, fps, df, reader source), health messages.
- Client: LTC card shows Running · <rate> / Stopped (held frame) /
  No signal; works LTC-only with no ProPresenter host configured; the
  reader-fed card no longer grays out when only ProPresenter is down.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 14:52:20 -04:00

256 lines
8.4 KiB
CSS

/* ProPresenter Timers module — scoped to its tiles, shell variables only.
The API exposes no per-timer color, so a card's color IS its state:
accent = running, muted = stopped, warn = complete, danger = overrun.
Overrun must be unmistakable at a distance — danger fill + pulse. */
[data-module="propresenter-timers"] .tm-wrap {
--tm-time-size: 32px;
--tm-ltc-size: 26px;
--tm-meta-size: 11px;
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
user-select: none;
-webkit-user-select: none;
}
/* ── degraded-state banner (feed lost / ProPresenter unreachable) ── */
[data-module="propresenter-timers"] .tm-banner {
flex: none;
margin: 6px 8px 0;
padding: 4px 10px;
border-radius: 6px;
background: color-mix(in srgb, var(--danger) 14%, transparent);
color: var(--danger);
font-size: 12px;
font-weight: 600;
text-align: center;
}
[data-module="propresenter-timers"] .tm-banner[hidden] { display: none; }
/* Values still on screen while the feed is down are stale — say so visually.
Per-card, because the reader-fed LTC card is not stale when only
ProPresenter is unreachable; it dims only when the ProdDash feed itself
is down (is-feed-down) and everything on screen really is old. */
[data-module="propresenter-timers"] .tm-wrap.is-stale .tm-card { opacity: 0.45; }
[data-module="propresenter-timers"] .tm-wrap.is-stale:not(.is-feed-down) .tm-card-ltc.src-reader { opacity: 1; }
[data-module="propresenter-timers"] .tm-hint {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
color: var(--muted);
font-size: 13px;
text-align: center;
}
[data-module="propresenter-timers"] .tm-hint[hidden] { display: none; }
/* ── card grid (column count set by client.js fit()) ── */
[data-module="propresenter-timers"] .tm-grid {
flex: 1;
min-height: 0;
display: grid;
grid-auto-rows: 1fr;
gap: var(--tm-gap, 8px);
padding: var(--tm-pad, 10px);
}
[data-module="propresenter-timers"] .tm-grid[hidden] { display: none; }
/* Compact cards (short tiles): the state line goes, its color stays.
The one exception is the LTC card with no timecode API behind it —
that message must never be hidden. */
[data-module="propresenter-timers"] .tm-compact .tm-card .tm-state { display: none; }
[data-module="propresenter-timers"] .tm-compact .tm-card-ltc.is-ltc-unavailable .tm-state { display: block; }
[data-module="propresenter-timers"] .tm-compact .tm-card.is-missing .tm-state { display: block; }
/* ── one timer card ── */
[data-module="propresenter-timers"] .tm-card {
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2%;
padding: 4px 8px;
border: 1px solid var(--border);
border-radius: 10px;
background: color-mix(in srgb, var(--bg) 60%, transparent);
overflow: hidden;
transition: opacity 0.3s ease;
}
[data-module="propresenter-timers"] .tm-name {
flex: none;
max-width: 100%;
display: flex;
align-items: center;
gap: 0.5em;
font-size: var(--tm-meta-size);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
[data-module="propresenter-timers"] .tm-time {
flex: none;
font-size: var(--tm-time-size);
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
line-height: 1.05;
color: var(--text);
white-space: nowrap;
}
[data-module="propresenter-timers"] .tm-state {
flex: none;
max-width: 100%;
font-size: var(--tm-meta-size);
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── state looks ── */
[data-module="propresenter-timers"] .tm-card.is-running {
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
[data-module="propresenter-timers"] .tm-card.is-running .tm-time { color: var(--accent); }
[data-module="propresenter-timers"] .tm-card.is-running .tm-state { color: var(--accent); }
[data-module="propresenter-timers"] .tm-card.is-stopped .tm-time { color: var(--muted); }
[data-module="propresenter-timers"] .tm-card.is-complete {
border-color: color-mix(in srgb, var(--warn) 55%, var(--border));
background: color-mix(in srgb, var(--warn) 10%, transparent);
}
[data-module="propresenter-timers"] .tm-card.is-complete .tm-time { color: var(--warn); }
[data-module="propresenter-timers"] .tm-card.is-complete .tm-state { color: var(--warn); }
/* A solo tile whose timer was deleted in ProPresenter. */
[data-module="propresenter-timers"] .tm-card.is-missing { border-style: dashed; }
[data-module="propresenter-timers"] .tm-card.is-missing .tm-time { color: var(--muted); }
[data-module="propresenter-timers"] .tm-card.is-missing .tm-state { color: var(--warn); }
/* Overrun: readable across the room even on a wall display. */
[data-module="propresenter-timers"] .tm-card.is-overrun {
border-color: var(--danger);
background: color-mix(in srgb, var(--danger) 22%, transparent);
animation: tm-overrun-pulse 1.2s ease-in-out infinite;
}
[data-module="propresenter-timers"] .tm-card.is-overrun .tm-time { color: var(--danger); }
[data-module="propresenter-timers"] .tm-card.is-overrun .tm-state {
color: var(--danger);
font-weight: 800;
}
@keyframes tm-overrun-pulse {
50% { background: color-mix(in srgb, var(--danger) 38%, transparent); }
}
/* ── the LTC card: visually its own thing ── */
[data-module="propresenter-timers"] .tm-card-ltc {
border-style: dashed;
}
[data-module="propresenter-timers"] .tm-card-ltc .tm-time {
font-size: var(--tm-ltc-size);
}
[data-module="propresenter-timers"] .tm-ltc-badge {
flex: none;
padding: 0.15em 0.55em;
border-radius: 5px;
background: color-mix(in srgb, var(--accent) 24%, transparent);
color: var(--text);
font-size: 0.95em;
letter-spacing: 0.12em;
}
[data-module="propresenter-timers"] .tm-card-ltc.is-ltc-receiving {
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
[data-module="propresenter-timers"] .tm-card-ltc.is-ltc-receiving .tm-time { color: var(--accent); }
[data-module="propresenter-timers"] .tm-card-ltc.is-ltc-receiving .tm-state { color: var(--accent); }
[data-module="propresenter-timers"] .tm-card-ltc.is-ltc-nosignal {
border-color: color-mix(in srgb, var(--warn) 55%, var(--border));
}
[data-module="propresenter-timers"] .tm-card-ltc.is-ltc-nosignal .tm-time { color: var(--muted); }
[data-module="propresenter-timers"] .tm-card-ltc.is-ltc-nosignal .tm-state {
color: var(--warn);
animation: tm-nosignal-blink 1.6s steps(2) infinite;
}
@keyframes tm-nosignal-blink { 50% { opacity: 0.35; } }
/* LTC ceased mid-show: hold the last frame, quietly — no alarm colors,
stopping the clock is usually somebody doing their job. */
[data-module="propresenter-timers"] .tm-card-ltc.is-ltc-stopped .tm-time { color: var(--muted); }
/* No timecode API on this ProPresenter: say so, quietly. */
[data-module="propresenter-timers"] .tm-card-ltc.is-ltc-unavailable .tm-time { color: var(--muted); }
[data-module="propresenter-timers"] .tm-card-ltc.is-ltc-unavailable .tm-state {
text-transform: none;
letter-spacing: 0.02em;
white-space: normal;
text-align: center;
}
[data-module="propresenter-timers"] .tm-card-ltc.is-ltc-waiting .tm-time { color: var(--muted); }
/* ── the "Timers ▾" menu (container styled/positioned by the shell) ── */
[data-module="propresenter-timers"] .tm-menu-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 6px 9px;
background: none;
border: none;
border-radius: 6px;
color: var(--text);
font-size: 12px;
font-family: inherit;
text-align: left;
cursor: pointer;
white-space: nowrap;
}
[data-module="propresenter-timers"] .tm-menu-item:hover { background: var(--border); }
[data-module="propresenter-timers"] .tm-menu-item .check {
width: 14px;
flex: none;
color: var(--accent);
font-weight: 700;
visibility: hidden;
}
[data-module="propresenter-timers"] .tm-menu-item.on .check { visibility: visible; }
[data-module="propresenter-timers"] .tm-menu-item .name { font-weight: 600; }
[data-module="propresenter-timers"] .tm-menu-note {
padding: 6px 9px;
color: var(--muted);
font-size: 12px;
white-space: nowrap;
}