fix(slideshow): present media controls (#1601)

* fix(slideshow): harden media controls in present decks

* refactor(slideshow): clear Fallow audit findings

Decompose flagged high-CRAP functions and extract production-code
duplications so the audit gate clears.

- core/runtime/bridge.ts handler — replace the 14-branch if-chain with a
  CONTROL_HANDLERS dispatch table; flash-elements payload handling moves
  to its own helper. Behavior preserved (all existing bridge.test.ts
  cases hit the same dispatchers via the public installRuntimeControlBridge
  API).

- player/slideshow/SlideshowController syncTo — split into
  isValidSyncTarget / isCrossSlide / rerootStackTo helpers. The
  stopSlideMedia decision and the stack re-rooting are now individually
  named; the public method is a 4-line orchestrator.

- cli/commands/validate.ts run — extract emitJsonReport / emitTextReport
  so the orchestrator no longer carries the dual JSON/text branches.
  Cuts the cyclomatic complexity flagged by fallow after the
  shouldIgnoreRequestFailure signature expansion shifted the fingerprint.

- player/hyperframes-player.ts — _setIframeMediaMuted and _stopIframeMedia
  shared a `try { iframeDoc = contentDocument } catch { return }` preamble
  (clone group 15). Extract _getSameOriginIframeDocument(): Document | null
  and have both call sites consume it.

- studio/panels/SlideshowPanel.tsx — the notes controller's debounce-tail
  and explicit flush() shared the pending-drain pattern (clone group 16).
  Extract a drainPending() closure both call.

- player/hyperframes-player.test.ts — collapse the new stopMedia / muted
  tests' repeated Object.defineProperty(iframe, "contentDocument", { get })
  shape behind a stubIframeContentDocument helper.

No behavior changes — refactor only. Existing tests cover the affected
paths unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(validate): split run further; ignore test dup parity

Second Fallow pass surfaced two minor follow-ups after the first cut:

- packages/cli/src/commands/validate.ts run + emitTextReport still
  carried minor CRAP findings (43.1 / 37.1, threshold 30). Extract
  printValidationResult / formatConsoleEntry / formatTotals /
  emitFailureReport so run becomes a try/catch + delegation, well
  below the threshold; emitTextReport drops the inline format loops.

- .fallowrc.jsonc duplicates.ignore: add hyperframes-player.test.ts
  alongside the existing SlideshowPanel.test.ts entry. Same reasoning
  documented there — parallel arrange/act/assert test cases are
  intentionally self-contained for readability; collapsing them under
  shared fixtures would couple unrelated scenarios (same-origin vs
  realm media, audio-locked permutations, seek bridge variants).

No behavior changes — refactor + config-policy parity only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-06-19 17:17:39 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent cd832f01ac
commit f0c4dee705
23 changed files with 959 additions and 371 deletions
+16 -2
View File
@@ -174,7 +174,7 @@ function buildPresentPage(projectName: string, islandJson: string): string {
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${projectName} — Presenter</title>
<title>${escHtml(projectName)} — Presenter</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; background: #0a0a0a; overflow: hidden; }
@@ -193,7 +193,7 @@ function buildPresentPage(projectName: string, islandJson: string): string {
</head>
<body>
<hyperframes-slideshow tabindex="0" sound>
<hyperframes-player src="/composition/index.html"></hyperframes-player>
<hyperframes-player interactive src="/composition/index.html"></hyperframes-player>
<script type="application/hyperframes-slideshow+json">
${islandJson}
</script>
@@ -241,10 +241,16 @@ ${islandJson}
// Mute state is owned by <hyperframes-slideshow sound>; mirror it.
var muted = false;
function setClipsMuted(nextMuted) {
Object.keys(clips).forEach(function (name) {
clips[name].muted = nextMuted;
});
}
var ss = document.querySelector("hyperframes-slideshow");
if (ss) {
ss.addEventListener("hf-sound", function (e) {
muted = e.detail && e.detail.muted === true;
setClipsMuted(muted);
});
}
@@ -295,3 +301,11 @@ ${islandJson}
</body>
</html>`;
}
function escHtml(value: string): string {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}