Files
rm-circle-team-router/public/tool-video.js
T
martbost d5385b0512 Poster frames for the walkthrough videos
The player was showing a blank white rectangle, which reads as broken. That
is frame zero of each recording — the instant before the page painted. Only
frame zero is white (by 0.5s the real UI is up), but frame zero is exactly
what a browser picks when no poster is set.

Each video now has a poster pulled from ~30% in, where the tool is in a
meaningful state rather than mid-intro. Posters live beside the videos in the
volume and the player derives the URL from the video's own, so adding a new
walkthrough needs no page change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 14:43:17 -05:00

120 lines
5.2 KiB
JavaScript

// Collapsible walkthrough player for Suite tool pages.
//
// Placement reasoning (Marty's call): the video belongs at the TOP of the tool,
// because a video in a separate library never gets watched — but it must not be
// an embedded player, because after the first week most visits are returning
// members who came to USE the tool, and a player pushes the tool below the fold
// on mobile every single time.
//
// So: a one-line bar that expands inline, auto-opened on the member's FIRST
// visit to that specific tool and collapsed on every visit after. The state is
// per-tool, per-browser. localStorage can throw outright in some contexts
// (private windows, blocked site data), so every access is guarded and the bar
// falls back to "collapsed but present" rather than breaking the page.
(function () {
'use strict';
var el = document.currentScript;
var SRC = el && el.getAttribute('data-video');
var TITLE = (el && el.getAttribute('data-title')) || 'Watch the walkthrough';
var KEY = 'rmc.tv.' + ((el && el.getAttribute('data-key')) || (location.pathname.replace(/\W+/g, '') || 'tool'));
var SECS = (el && el.getAttribute('data-secs')) || '';
if (!SRC) return;
function seen() { try { return localStorage.getItem(KEY) === '1'; } catch (e) { return true; } }
function markSeen() { try { localStorage.setItem(KEY, '1'); } catch (e) {} }
function build() {
var host = document.querySelector('main') || document.body;
var wrap = document.createElement('div');
wrap.className = 'tv-wrap';
var bar = document.createElement('button');
bar.type = 'button';
bar.className = 'tv-bar';
bar.setAttribute('aria-expanded', 'false');
bar.innerHTML = '<span class="tv-play">▶</span><span class="tv-t">' + TITLE +
(SECS ? ' <span class="tv-secs">' + SECS + '</span>' : '') + '</span><span class="tv-chev">⌄</span>';
var body = document.createElement('div');
body.className = 'tv-body';
body.hidden = true;
var vid = document.createElement('video');
vid.controls = true;
// Poster lives beside the video (same slug, .jpg). Without it the browser
// shows frame zero, and frame zero is the instant before the page painted
// — a blank white rectangle that reads as a broken player.
vid.poster = SRC.replace(/\.mp4$/, '.jpg');
// Nothing is fetched until the panel is actually opened; open() then bumps
// this to 'metadata' so an expanded panel shows a real first frame rather
// than a black rectangle.
vid.preload = 'none';
vid.playsInline = true;
vid.src = SRC;
body.appendChild(vid);
function open() {
body.hidden = false;
wrap.classList.add('on');
bar.setAttribute('aria-expanded', 'true');
// Deliberately NOT autoplaying. Browsers only permit autoplay when the
// video is muted, and a silent walkthrough is worthless here — the
// narration IS the training. So on first visit the panel opens with the
// video paused and the play button showing; pressing it is a user
// gesture, which means it plays with sound, every time.
vid.preload = 'metadata';
}
function close() {
body.hidden = true;
wrap.classList.remove('on');
bar.setAttribute('aria-expanded', 'false');
vid.pause();
}
bar.addEventListener('click', function () {
if (body.hidden) { open(); markSeen(); }
else close();
});
wrap.appendChild(bar);
wrap.appendChild(body);
// Sit directly under the page heading if there is one, otherwise at the top.
var h1 = host.querySelector('h1');
var lede = h1 && h1.nextElementSibling && /lede|sub/.test(h1.nextElementSibling.className || '')
? h1.nextElementSibling : h1;
if (lede && lede.parentNode) lede.parentNode.insertBefore(wrap, lede.nextSibling);
else host.insertBefore(wrap, host.firstChild);
if (!seen()) { open(); markSeen(); }
}
function style() {
if (document.getElementById('tv-style')) return;
var s = document.createElement('style');
s.id = 'tv-style';
s.textContent = [
'.tv-wrap{margin:0 0 18px}',
'.tv-bar{display:flex;align-items:center;gap:10px;width:100%;text-align:left;cursor:pointer;',
' background:rgba(212,175,55,.08);border:1px solid var(--gold,#d4af37);border-radius:12px;',
' color:var(--text,#e8eef4);padding:11px 15px;font:inherit;font-size:14.5px;font-weight:700}',
'.tv-bar:hover{background:rgba(212,175,55,.14)}',
'.tv-bar:focus-visible{outline:2px solid var(--teal,#4ed6cb);outline-offset:2px}',
'.tv-play{color:var(--gold,#d4af37);font-size:12px}',
'.tv-t{flex:1}',
'.tv-secs{color:var(--muted,#8fa6bb);font-weight:600;font-size:12.5px}',
'.tv-chev{color:var(--muted,#8fa6bb);transition:transform .18s}',
'.tv-wrap.on .tv-chev{transform:rotate(180deg)}',
'.tv-body{margin-top:10px;border:1px solid var(--line,#24425d);border-radius:12px;overflow:hidden;line-height:0}',
'.tv-body video{width:100%;height:auto;display:block;background:#000}',
'@media (prefers-reduced-motion:reduce){.tv-chev{transition:none}}'
].join('');
document.head.appendChild(s);
}
function go() { style(); build(); }
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', go);
else go();
})();