fix(slideshow): finish remaining split-PR review findings (#1594)

* fix(slideshow): address split-PR review findings on #1585

Genuinely-open findings from the #1580/#1590/#1591/#1592 reviews (the rest were
already fixed on this branch: CSP handlers, manifest version, UUID ids, float
keys, presenter 1s-timer):

core (#1580):
- isManifest rejects a non-object/array manifest (e.g. [42,null]) explicitly
- resolveSlideshow flags duplicate slideSequence ids instead of silent overwrite

player (#1590):
- present() window.open uses noopener,noreferrer (audience syncs via channel)
- BroadcastChannel name is per-deck (keyed on pathname) to avoid same-origin
  cross-talk between decks
- add observedAttributes + attributeChangedCallback so runtime sound/mode toggles
  re-render

studio (#1591/#1592):
- persistSlideshowManifest no-op gate (skip write when HTML is unchanged)
- surface persist failures (console.error) instead of silent .catch(()=>{})
- confirm before deleting a branch sequence (data-loss + dangling hotspots)

+ tests for the collision + non-object-manifest rejection. 20 core / 106 player /
53 studio pass; tsc/lint/fmt/fallow clean; deck still renders.

* fix(slideshow): finish remaining split-PR review findings

The larger items from the #1580/#1590/#1591/#1592 reviews (the rest landed in
#1585):

core (#1580):
- dedup isSceneLikeCompositionId — shared slideshow/sceneId.ts, used by both the
  lint rule and the runtime scene-window computation (no more mirror-and-drift)

player (#1590 / #1592):
- onKey: when multiple decks share a page, drop the unfocused-convenience so a
  key drives only the focused deck
- slow-iframe recovery: if the scene timeline posts after the wait times out
  (empty scenes), re-init once so sceneId slides resolve instead of being dropped

studio (#1591):
- persistSlideshowManifest validates the built island round-trips before writing
- reorderBranchSlide helper + BranchTree up/down controls (parallel to main-line
  reorder), with a branch-position indicator

+ tests for reorderBranchSlide. core 228 / player 106 / studio (panel) 46 pass;
tsc/lint/fmt/fallow clean.

* fix(player,cli): use fileURLToPath for path resolution (Windows CI)

new URL(...).pathname yields a leading-slash drive path ("/D:/...") on Windows,
which broke:
- packages/player/vitest.config.ts — the @hyperframes/core/slideshow alias
  resolved to a nonexistent path, failing the player slideshow tests on the
  Windows render-verification CI (passed on macOS/Linux where pathname is clean)
- packages/cli/src/utils/compositionServer.ts helperDir — same bug in the
  play/present bundle-path resolution

fileURLToPath converts file:// URLs to correct OS paths on all platforms.
Player slideshow tests pass; present serves + resolves bundles.

* fix(producer): fileURLToPath for the renders dir (Windows)

DEFAULT_RENDERS_DIR used new URL(import.meta.url).pathname, which is "/D:/..."
on Windows and resolves to a bogus path — affects the Windows render pipeline.
Last of the .pathname -> fileURLToPath fixes (repo-wide src sweep now clean).
This commit is contained in:
Vance Ingalls
2026-06-19 04:31:12 -07:00
committed by GitHub
parent cc2220e59e
commit f05b3f9c7c
16 changed files with 240 additions and 54 deletions
@@ -114,6 +114,17 @@ export class HyperframesSlideshow extends HTMLElement {
}
}
// Observe the attributes the component reads so runtime toggles take effect.
static get observedAttributes(): string[] {
return ["sound", "mode"];
}
attributeChangedCallback(): void {
// Re-render once bound so a flipped `sound`/`mode` is reflected (mute button,
// audience-vs-presenter chrome). No-op before the controller binds.
if (this.controller) this.render();
}
connectedCallback(): void {
this.disconnected = false;
this.initInFlight = false;
@@ -178,7 +189,9 @@ export class HyperframesSlideshow extends HTMLElement {
*/
present(): void {
const sep = location.search ? "&" : "?";
window.open(location.href + sep + "mode=audience", "_blank");
// noopener,noreferrer: the audience window must not get a reference back to
// this window (it syncs over BroadcastChannel, not window.opener).
window.open(location.href + sep + "mode=audience", "_blank", "noopener,noreferrer");
this.setAttribute("data-hf-presenting", "true");
this.presenterStartMs = Date.now();
if (this.presenterInterval === null) {
@@ -278,6 +291,19 @@ export class HyperframesSlideshow extends HTMLElement {
};
this.bindController(new SlideshowController(port, cleaned));
// Slow-iframe recovery: if the scene timeline hadn't posted yet (empty
// scenes → sceneId-based slides were dropped), re-init once when it finally
// arrives so those slides resolve instead of being permanently lost.
if (scenes.length === 0 && manifest.slides.length > 0) {
playerEl.addEventListener(
"scenes",
() => {
if (gen === this.initGeneration) void this.init();
},
{ once: true },
);
}
} finally {
this.initInFlight = false;
}
@@ -318,7 +344,10 @@ export class HyperframesSlideshow extends HTMLElement {
// Arrows act even when nothing is focused (active === body/null) so a freshly
// loaded deck responds without a click; Space/Backspace have strong page-level
// defaults (scroll / history) so they only act when the deck actually has focus.
const ambient = focused || active === document.body || active === null;
// When several decks share a page, drop the unfocused-convenience so a key
// doesn't drive every instance at once — only the focused deck responds.
const multiInstance = document.querySelectorAll("hyperframes-slideshow").length > 1;
const ambient = focused || (!multiInstance && (active === document.body || active === null));
if (e.key === "ArrowRight") {
if (!ambient) return;
this.controller.next();