feat: Phase 17.3 — Fixed embed queue panel with collapse/expand iframe resize (OQ1 Option A)

Read-only inline queue panel below the release embed's player bar; row-jump reuses PlayRelease. ForRelease mints a taller iframe plus a postMessage resize listener for the collapse toggle; ForTrack unchanged.
This commit is contained in:
daniel-c-harvey
2026-06-19 16:21:45 -04:00
parent fbd298b9c3
commit 466084b5a3
6 changed files with 257 additions and 21 deletions
@@ -0,0 +1,31 @@
// Embed (iframe) → host resize handshake (Phase 17 wave 17.3, OQ1 Option A).
//
// The Fixed-mode player renders an always-shown queue panel below the controls. A collapse/expand
// toggle lets the embedder's viewer reclaim the panel's vertical space — but collapsing inside the
// iframe only reclaims space if the *outer* iframe element also shrinks. The iframe cannot resize
// itself, so it posts its desired pixel height to the host page; the embed snippet (minted by
// EmbedSnippetBuilder.ForRelease) carries a tiny listener that sets iframe.style.height.
//
// Degrades safely: if the host page ignores or strips the snippet's listener (Option B's value), the
// panel still renders and toggles inside the iframe — only the outer resize is lost. We post nothing
// when not framed (window === parent), so the docked player is unaffected.
const MESSAGE_TYPE = "deepdrft-embed-resize";
/**
* Measure the live rendered height of the player element and post it to the host page so it can size
* the iframe to match. No-op when not embedded in a frame, or when the element is unmeasurable.
*
* targetOrigin is "*" deliberately: the embedder's origin is unknown (any blog can embed us) and the
* payload carries no secrets — just a height the host is free to ignore.
*/
export function postHeight(element: HTMLElement): void {
if (window.parent === window) return; // Not framed — nothing to resize.
if (!element) return;
// ceil + a hairline guard against sub-pixel rounding that would otherwise clip the bottom edge.
const height = Math.ceil(element.getBoundingClientRect().height) + 2;
if (!Number.isFinite(height) || height <= 0) return;
window.parent.postMessage({ type: MESSAGE_TYPE, height }, "*");
}
@@ -905,3 +905,15 @@ body:has(.deepdrft-queue-overlay) {
background: color-mix(in srgb, var(--deepdrft-green-accent) 22%, transparent);
border-radius: 6px;
}
/* ── Fixed (embed) inline queue panel (Phase 17 §4, OQ6). ──
Rendered below the player controls inside the embed surface. A fixed sensible height with internal
scroll past N rows (NOT grow-to-cap): ~4.5 rows are visible, the rest scroll. A top hairline
separates it from the controls. The list rows reuse the shared .deepdrft-queue-* styles above. */
.deepdrft-queue-embed-panel {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--deepdrft-border-light);
max-height: 184px;
overflow-y: auto;
}