* feat(slideshow): per-slide autoplay (manual-advance, opt-in)
Adds an opt-in `autoplay` flag to slideshow slides: when the presenter lands
on a video slide, its `<video>` plays from the start. The slideshow still
holds and never auto-advances — the presenter clicks Next when ready. This
covers compositions whose own controls can't be clicked (the player renders
the composition pointer-events:none).
Plumbing (done, tested):
- core: `SlideRef.autoplay?: boolean`, parsed + validated in parseSlideshow
(a non-boolean autoplay rejects the manifest); carried through resolve.
- controller: optional `PlayerPort.playSceneMedia(sceneId)`, fired only on
forward `enterSlide` for autoplay slides (not resume/back/sync, so the
audience — which mirrors the presenter's media events — isn't double-driven).
- component: `playSceneDocumentMedia` reaches the same-origin composition
iframe, finds the scene's `<video>`, and asserts playback; `stopMedia`
(already wired on slide change) resets it. An autoplay token cancels a
pending start when the slide changes.
- tests: controller autoplay behavior + parser flag round-trip/validation
(131 player + 22 core slideshow tests pass).
KNOWN LIMITATION — runtime media-start needs the player media model (@vance):
On current main the clip<->timeline binding from #1601 keeps every clip synced
and *paused* to the held timeline frame, which wins against playSceneMedia's
play() — so the clip does not actually start on main yet (it does on the
pre-#1601 player). The correct fix is a sanctioned "let this clip free-run
while the timeline holds" path in the player/runtime media controller. Flagging
for Vance to wire the start into the #1601 media model (or rebase onto it) when
back. The plumbing above is the stable surface that hook plugs into.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(slideshow): address code-review on per-slide autoplay
- guard playSceneDocumentMedia behind resolveMode() !== "audience": the
audience mirrors the presenter's media events, so it must not independently
drive its own copy of the clip.
- drop the per-enter window pointerdown/keydown "gesture retry" listeners,
which leaked when muted autoplay succeeded without a gesture. The poll already
re-asserts play(), so a gesture within the window is picked up next tick.
- stop polling once the clip is advancing across two ticks (was re-asserting
play() for the full window even after playback was confirmed).
- cancel any in-flight autoplay loop on disconnectedCallback (bump the token).
- split the poll into findSceneVideo + stepAutoplay helpers (keeps each small).
- fix the enterSlide comment: autoplay fires from enterSlide (next/prev/
goToSlide), not resumeSlide (back/backToMain/syncTo).
- parser: isOptionalBoolean type guard instead of a one-off helper; drop `as`
assertions in the new controller test.
131 player + 22 core slideshow tests pass; lint/format/typecheck/fallow clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(slideshow): autoplay skill guidance + address review nits
Addresses review feedback on #1708:
- skill: document per-slide `autoplay` in the slideshow standalone-harness
reference — when to use it (video is the slide's primary content, its end is
the advance cue) vs not (background/ambient loops, footage talked over), per
Vance's guidance, before merge.
- play() rejection is no longer blanket-swallowed: AbortError (timeline-sync
seek interrupt) and NotAllowedError (gesture-gated autoplay) are expected and
ignored; any other rejection is surfaced once via console.warn (Via nit 1).
- clarify in the SlideRef.autoplay doc that it plays the scene's FIRST <video>
(Via nit 2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* feat(player): slideshow controller state machine
Stack-split from the original ss-player PR (#1581): the SlideshowController
state machine (stack-based slide/fragment/branch navigation) and its tests.
The <hyperframes-slideshow> web component follows in the next PR.
* feat(player): <hyperframes-slideshow> web component + presenter
Stack-split from the original ss-player PR (#1581): the <hyperframes-slideshow>
custom element (wraps <hyperframes-player>, drives the controller),
presenter/audience BroadcastChannel sync, nav chrome, and the player scenes hook.
* feat(studio): slideshow manifest persistence + panel helpers
Stack-split from the original ss-studio PR (#1582): the data layer —
setSlideshowManifest, the useSlideshowPersist hook, and panel helpers.
The editor panel UI follows in the next PR.
* feat(studio): slideshow branching editor panel UI
Stack-split from the original ss-studio PR (#1582): the SlideshowPanel /
SlideshowSubPanels editor UI, right-panel wiring, and app integration.
* docs(skill): slideshow authoring guidance + standalone harness reference
New /slideshow skill (island schema, slide rules, fragments, branching,
validation) + a standalone-harness reference doc, and a router entry in
the /hyperframes skill.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>