Overflow Tap 1.0.0: switch Overflow Tap destinations from keywords in ProPresenter slide notes
A new module that does what Overflow's "ProPresenter Integration" helper app
does, inside ProdDash: the live slide's notes are read from ProPresenter
(GET /v1/status/slide, twice a second) and when they carry one of the admin
page's keywords, the chosen Overflow Tap groups are pointed at that keyword's
destination. One tile: the keyword Tap is on (and since when), its
destination and the groups moved, a chip saying whether the slide on screen
carries a keyword, and the last four switches. A failed switch turns the tile
red with the reason, retries three times, and can flash an alert channel.
Admin config: ProPresenter endpoint + password; Overflow client ID + API key
(x-client-id / x-api-key, the public API's two headers); Tap groups to switch
(multiselect read from the account over /groups); keywords as text, one per
line (`give = Give Online`, optionally `@ Lobby, Kids Wing`); match mode
(whole word / anywhere / own line); alerts switch + channel.
The one unverified piece: Overflow's public API documents only reads for Tap
(OpenAPI checked 2026-10-07), while Overflow's helper uses the Dashboard's
private API. The switch is sent as PATCH /api/v3/tap/groups/{id} with the new
destinationId — the natural shape for this API — and every switch is confirmed
by reading the group back, so a refusal shows on the tile instead of passing
silently. overflow.js is the one place to change if Overflow names a
different route.
Also: tools/overflow-tap-mock.js (a ProPresenter whose slide notes carry
keywords on :1602 plus the Overflow Tap API on :24720, --read-only to fail
every switch), keywords.selftest.js (14 cases), README + CLAUDE.md entries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c9e03398a6
commit
6b51044be6
@@ -0,0 +1,213 @@
|
||||
/* Overflow Tap — scoped under its tile via [data-module="overflow-tap"] and
|
||||
styled with the shell's variables only. Sizes hang off the tile (container
|
||||
units) so the keyword reads from across a booth on a big tile and still
|
||||
fits a 2×2. */
|
||||
|
||||
[data-module="overflow-tap"] {
|
||||
container-type: size;
|
||||
}
|
||||
|
||||
[data-module="overflow-tap"] .ot-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: clamp(4px, 4cqmin, 18px);
|
||||
gap: clamp(2px, 1.8cqmin, 10px);
|
||||
overflow: hidden;
|
||||
color: var(--text);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
[data-module="overflow-tap"] .ot-offline {
|
||||
flex-shrink: 0;
|
||||
font-size: clamp(0.6rem, 4.6cqmin, 0.9rem);
|
||||
font-weight: 700;
|
||||
color: var(--danger);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
[data-module="overflow-tap"] .ot-root.is-offline .ot-main,
|
||||
[data-module="overflow-tap"] .ot-root.is-offline .ot-slide,
|
||||
[data-module="overflow-tap"] .ot-root.is-offline .ot-history {
|
||||
opacity: 0.45;
|
||||
filter: grayscale(0.6);
|
||||
}
|
||||
|
||||
/* ── the live keyword ───────────────────────────────────────────── */
|
||||
|
||||
[data-module="overflow-tap"] .ot-main {
|
||||
--tone: var(--accent);
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
container-type: size;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
[data-module="overflow-tap"] .ot-main[data-tone="ok"] { --tone: var(--accent); }
|
||||
[data-module="overflow-tap"] .ot-main[data-tone="warn"] { --tone: var(--warn); }
|
||||
[data-module="overflow-tap"] .ot-main[data-tone="alert"] { --tone: var(--danger); }
|
||||
[data-module="overflow-tap"] .ot-main[data-tone="muted"] { --tone: var(--muted); }
|
||||
|
||||
[data-module="overflow-tap"] .ot-label {
|
||||
color: var(--tone);
|
||||
font-size: clamp(0.5rem, 9cqh, 0.95rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
line-height: 1.2;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
[data-module="overflow-tap"] .ot-keyword {
|
||||
color: var(--tone);
|
||||
font-size: min(clamp(1rem, 38cqh, 7rem), 18cqw);
|
||||
font-weight: 750;
|
||||
line-height: 1.15;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
[data-module="overflow-tap"] .ot-keyword.is-none { color: var(--muted); }
|
||||
[data-module="overflow-tap"] .ot-main[data-tone="muted"] .ot-keyword {
|
||||
animation: ot-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes ot-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.45; }
|
||||
}
|
||||
|
||||
[data-module="overflow-tap"] .ot-dest {
|
||||
color: var(--text);
|
||||
font-size: min(clamp(0.7rem, 16cqh, 1.9rem), 8cqw);
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
[data-module="overflow-tap"] .ot-main[data-tone="muted"] .ot-dest { color: var(--muted); font-weight: 500; }
|
||||
|
||||
[data-module="overflow-tap"] .ot-groups {
|
||||
color: var(--muted);
|
||||
font-size: clamp(0.5rem, 9cqh, 0.95rem);
|
||||
line-height: 1.3;
|
||||
flex-shrink: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-module="overflow-tap"] .ot-error {
|
||||
color: var(--danger);
|
||||
font-size: clamp(0.5rem, 9cqh, 0.95rem);
|
||||
line-height: 1.3;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
[data-module="overflow-tap"] .ot-error.is-warn { color: var(--warn); }
|
||||
|
||||
/* ── this slide ─────────────────────────────────────────────────── */
|
||||
|
||||
[data-module="overflow-tap"] .ot-slide {
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
font-size: clamp(0.55rem, 4.8cqmin, 0.95rem);
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0.25em 0.6em;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--text) 7%, transparent);
|
||||
align-self: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
[data-module="overflow-tap"] .ot-slide.is-muted { color: var(--muted); }
|
||||
[data-module="overflow-tap"] .ot-slide.is-hit {
|
||||
color: var(--accent-text);
|
||||
background: color-mix(in srgb, var(--accent) 80%, transparent);
|
||||
font-weight: 650;
|
||||
}
|
||||
[data-module="overflow-tap"] .ot-slide.is-warn {
|
||||
color: var(--warn);
|
||||
background: color-mix(in srgb, var(--warn) 16%, transparent);
|
||||
}
|
||||
|
||||
/* ── the trail ──────────────────────────────────────────────────── */
|
||||
|
||||
[data-module="overflow-tap"] .ot-history {
|
||||
flex-shrink: 0;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0.3em 0 0;
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: clamp(0.55rem, 4.6cqmin, 0.95rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1em;
|
||||
}
|
||||
|
||||
[data-module="overflow-tap"] .ot-h-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) minmax(0, 1.4fr) auto;
|
||||
align-items: baseline;
|
||||
gap: 0 0.7em;
|
||||
color: var(--text);
|
||||
}
|
||||
[data-module="overflow-tap"] .ot-h-row.is-failed .ot-h-kw,
|
||||
[data-module="overflow-tap"] .ot-h-row.is-failed .ot-h-mark { color: var(--danger); }
|
||||
[data-module="overflow-tap"] .ot-h-time {
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
[data-module="overflow-tap"] .ot-h-kw { font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
[data-module="overflow-tap"] .ot-h-dest { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
[data-module="overflow-tap"] .ot-h-mark { color: var(--accent); font-weight: 700; }
|
||||
[data-module="overflow-tap"] .ot-h-empty { color: var(--muted); text-align: center; }
|
||||
|
||||
/* On a short tile the trail would crowd the keyword out: keep the
|
||||
keyword, drop the trail. Inside the keyword block the same rule: when
|
||||
it is short, the small lines go before the keyword does. */
|
||||
@container (max-height: 120px) {
|
||||
[data-module="overflow-tap"] .ot-history { display: none; }
|
||||
}
|
||||
@container (max-height: 70px) {
|
||||
[data-module="overflow-tap"] .ot-groups,
|
||||
[data-module="overflow-tap"] .ot-error { display: none; }
|
||||
}
|
||||
@container (max-height: 46px) {
|
||||
[data-module="overflow-tap"] .ot-dest,
|
||||
[data-module="overflow-tap"] .ot-label { display: none; }
|
||||
}
|
||||
|
||||
[data-module="overflow-tap"] .ot-empty {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6%;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
font-size: clamp(0.65rem, 5.5cqmin, 1rem);
|
||||
}
|
||||
Reference in New Issue
Block a user