From f0c4dee70584f02a546c0cdb6d02d8b5542a4a4b Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Fri, 19 Jun 2026 17:17:39 -0700 Subject: [PATCH] fix(slideshow): present media controls (#1601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 --------- Co-authored-by: Claude Opus 4.7 --- .fallowrc.jsonc | 6 + packages/cli/src/commands/present.ts | 18 +- packages/cli/src/commands/validate.test.ts | 14 ++ packages/cli/src/commands/validate.ts | 158 ++++++++++------- packages/core/src/runtime/bridge.test.ts | 8 + packages/core/src/runtime/bridge.ts | 99 +++++------ packages/core/src/runtime/init.ts | 7 + packages/core/src/runtime/types.ts | 1 + .../player/src/hyperframes-player.test.ts | 149 +++++++++++++++- packages/player/src/hyperframes-player.ts | 43 ++++- packages/player/src/media-element-guards.ts | 14 ++ packages/player/src/parent-media.ts | 55 +++--- .../src/slideshow/SlideshowController.test.ts | 167 +++++++++++------- .../src/slideshow/SlideshowController.ts | 147 ++++++++------- .../slideshow/hyperframes-slideshow.test.ts | 84 +++++++++ .../src/slideshow/hyperframes-slideshow.ts | 100 ++++++++++- .../src/slideshow/slideshowPresenter.ts | 7 +- packages/player/src/styles.ts | 7 + packages/player/src/timeline-adapters.ts | 4 +- .../src/components/panels/SlideshowPanel.tsx | 38 ++-- .../components/panels/SlideshowSubPanels.tsx | 51 ++---- skills/slideshow/SKILL.md | 71 +++++++- .../references/standalone-harness.md | 82 ++++++++- 23 files changed, 959 insertions(+), 371 deletions(-) create mode 100644 packages/player/src/media-element-guards.ts diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index 09f615b95..27ae87f2c 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -247,6 +247,12 @@ // SlideshowPanel.test.ts: parallel arrange/act/assert test cases — collapsing // them would hurt readability of what each case verifies. "packages/studio/src/components/panels/SlideshowPanel.test.ts", + // hyperframes-player.test.ts: parallel arrange/act/assert test cases verifying + // distinct behaviors (same-origin vs realm media, audio-locked permutations, + // seek bridge variants). Each case is self-contained for readability; + // extracting the iframe / mock-audio setup helpers would over-couple + // unrelated scenarios under a shared fixture. + "packages/player/src/hyperframes-player.test.ts", // present.ts mirrors play.ts's server startup + console-output block. The // shared low-level pieces (resolve*/injectRuntime/listenOnFreePort) are in // utils/compositionServer.ts; the remaining clone is per-command logging text diff --git a/packages/cli/src/commands/present.ts b/packages/cli/src/commands/present.ts index 7fae6f49c..c96675adc 100644 --- a/packages/cli/src/commands/present.ts +++ b/packages/cli/src/commands/present.ts @@ -174,7 +174,7 @@ function buildPresentPage(projectName: string, islandJson: string): string { - ${projectName} — Presenter + ${escHtml(projectName)} — Presenter @@ -188,6 +244,7 @@ The key insight: scene backgrounds must be `transparent` (not opaque) if you wan if (!el) continue; var active = t >= s.start && t < s.end; el.style.opacity = active ? "1" : "0"; + el.style.visibility = active ? "visible" : "hidden"; el.style.pointerEvents = active ? "auto" : "none"; if (active && lastActiveId !== s.id) { @@ -334,7 +391,11 @@ Omitting any scene (including branch scenes) from this manifest means the slides ## 6. Audio/SFX — built-in mute control via `` -Audio **must** live in the parent page, not the composition iframe. Browsers enforce user-activation for AudioContext and HTMLAudioElement.play() — an iframe without its own activation (i.e., the user never clicked inside it) is permanently autoplay-blocked. The user's keypress lands on the parent, so the parent is the only frame that can get the activation token. +Wrapper-owned SFX should live in the parent page. Browsers enforce user-activation for AudioContext and HTMLAudioElement.play() — an iframe without its own activation (i.e., the user never clicked inside it) is often autoplay-blocked. The user's keypress lands on the parent, so the parent is the reliable frame for click/transition sound effects. + +Normal slide media should stay in the composition. The slideshow player now stops slide media automatically on slide/sequence changes by calling `hyperframes-player.stopMedia()`, which pauses iframe `