Skip to content

feat(core): slideshow schema, parser, and lint rule#1580

Merged
vanceingalls merged 1 commit into
mainfrom
ss-core
Jun 19, 2026
Merged

feat(core): slideshow schema, parser, and lint rule#1580
vanceingalls merged 1 commit into
mainfrom
ss-core

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Slideshow mode — 1/5: core schema, parser & lint

Foundation for slideshow mode: a composition can declare an embedded slideshow manifest that turns its continuous timeline into a discrete, navigable deck. This PR adds the data model, parser, and validation — no runtime/UI yet.

What & why

A slide is just an existing scene (data-composition-id + data-start/data-duration) plus metadata declared in one embedded <script type="application/hyperframes-slideshow+json"> island. Keeping the manifest in the composition means no new file format and no build step — slides are additive metadata over a normal composition.

Key changes

  • slideshow/slideshow.types.tsSlideshowManifest, SlideRef, SlideHotspot, SlideSequence and their resolved counterparts. TTS fields (ttsScript/ttsAudioUrl/ttsDurationMs) are present but reserved (playback not built).
  • slideshow/parseSlideshow.tsparseSlideshowManifest(html) extracts the island; resolveSlideshow(manifest, scenes) resolves each sceneId to a {start,end} range (honouring optional startTime/endTime overrides) and returns validation errors for: unresolved sceneId, fragment outside a slide's range, hotspot targeting an unknown sequence, and overlapping main-line slides.
  • lint/rules/slideshow.ts — surfaces those resolve errors under hyperframes lint; derives scenes from data-composition-id (matching the runtime's scene source).
  • lint/rules/core.ts — exempts the slideshow island MIME type from the inline-script-syntax check.
  • ./slideshow subpath export (dev + publishConfig) so downstream packages import only the lightweight parser, keeping core's Node-only barrel out of their typecheck graph.

Testing

parseSlideshow.test.ts + slideshow.test.ts (vitest) cover parse, resolution, every error path, and the lint rule.

Stack

Bottom of a 5-PR stack: core → player (#1581) → studio (#1582) → skill (#1583) → examples (#1584).

🤖 Generated with Claude Code

Slideshow manifest types, JSON-island parser/resolver (sceneId->time-range),
the slideshow lint rule, and a ./slideshow subpath export. Foundation for the
slideshow player and studio editor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

vanceingalls commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

export function parseSlideshowManifest(html: string): SlideshowManifest | null {
// Match <script type="application/hyperframes-slideshow+json"> ... </script>
const re = new RegExp(
`<script[^>]*type=["']${ISLAND_TYPE.replace(/[.+]/g, "\\$&")}["'][^>]*>([\\s\\S]*?)<\\/script>`,
vanceingalls added a commit that referenced this pull request Jun 19, 2026
- player: bundle @hyperframes/core into the IIFE/global build (noExternal)
- player: resolve audience mode from ?mode=audience URL query, not just attr
- player: event-driven waitForScenes + loud failure when no slides resolve
- player: scope window keydown so Space/Backspace don't hijack the host page
- player: audience mirrors full position (branch + fragment) via syncTo
- player: next() reveals remaining fragments even at slide end; enterBranch ignores empty sequences
- core: harden extractScenes against null/non-object scene entries
- core: strict manifest validation; error on inverted ranges & empty hotspot targets; dedup fragments
- core/lint: accept data-end/timeline-derived scene durations (match runtime)
- core+studio: share ISLAND_TYPE + island regex from @hyperframes/core/slideshow
- studio: SlideList reflects manifest slide order; branch-slide authoring (notes/fragments/hotspots)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat(core): slideshow schema, parser, and lint rule

Solid foundation PR. The "slides as metadata over existing scenes" design is clean — embedding the manifest as a typed JSON island inside the composition HTML avoids a new file format and keeps slides purely additive. The parser, resolver, and lint rule are well-structured and test coverage is thorough. A few things worth discussing before this merges as the base of a 10-PR stack:


1. Schema — types look good, one design question

The types are well-defined and properly constrained. SlideRefResolvedSlide progression is clean: optional fields become required, arrays get defaults. The extends SlideRef on ResolvedSlide is the right call — it means downstream consumers always get the superset.

Question on SlideHotspot.region: The { x, y, w, h } comment says "% of slide" — should this be enforced (0-100 range validation) in the resolver, or is that intentionally deferred to the player PR? If deferred, a // TODO: validate bounds in player comment would help, since this type will be the contract for 4 downstream PRs.

TTS fields: Flagging ttsScript/ttsAudioUrl/ttsDurationMs as "reserved — parsed and carried, never consumed" is the right approach. Consider adding @experimental or @reserved JSDoc tags so IDE tooltips surface this intent — right now it's only documented in the PR description, which evaporates from working memory fast.

2. Parser — solid, one edge case

Regex extraction (parseSlideshowManifest): The regex approach is consistent with how the linter extracts scripts elsewhere. The escaping of . and + in the MIME type is correct.

Edge case — multiple islands: RegExp.exec with no g flag returns the first match. If someone accidentally pastes two <script type="application/hyperframes-slideshow+json"> blocks, the second is silently ignored. Worth either (a) erroring on >1 match, or (b) documenting single-island-only as an intentional constraint. Given this is the foundation PR, establishing this invariant now prevents confusion in downstream PRs.

isManifest validator: This only checks typeof === "object" and Array.isArray(slides). It does not validate that each element of slides is a valid SlideRef (e.g., has a sceneId string). A manifest like {"slides": [42, null, "oops"]} passes isManifest and then blows up inside resolveSlide with an unhelpful error. Consider at minimum checking slides.every(s => typeof s?.sceneId === 'string').

3. Resolver — clean logic, two notes

Overlap detection: The adjacent-pair comparison after sorting by start is correct for detecting overlaps. One nuance: if two slides share the exact same start, the sort is not stable on end, so which one is "prev" vs "curr" is arbitrary. This doesn't affect correctness (they overlap either way), but the error message might name them in a surprising order. Not blocking, just noting.

Fragment validation boundary condition: f < start || f > end — a fragment exactly at end is allowed. Is that intentional? Fragments at the exact boundary of a slide's end time are technically at the transition point. If fragments represent "pause points within a slide," having one at the very last moment seems odd. If it's intentional, a comment clarifying the inclusive-end semantics would help.

4. Lint rule — well-integrated, one concern

Scene extraction uses data-composition-id only (not .clip[id]): This is explicitly tested and correct per the PR description — it mirrors the runtime's scene source. The test at line 29-35 of slideshow.test.ts validating that .clip[id] does NOT resolve is a great inclusion.

Concern — isSceneLikeCompositionId is duplicated: The comment says it "mirrors" packages/core/src/runtime/timeline.ts, but it's a full copy. If the runtime's heuristic changes (e.g., a new exclusion pattern), the lint rule silently diverges. Options: (a) import the canonical function, (b) add a // SYNC: comment pointing to the source of truth, (c) extract to a shared util. For a foundation PR, at minimum (b) would be good.

5. API surface

The ./slideshow subpath export is the right call — keeping the parser importable without pulling in the full core barrel is good for downstream bundle size. The re-export from src/index.ts provides convenience for consumers who already depend on core.

6. Security

No injection risk. The manifest is parsed from HTML the author controls, validated through JSON.parse (which is safe), and the regex only extracts content between matched script tags. No eval, no dynamic code execution, no user-supplied HTML interpretation. Clean.

7. Minor nits

  • slideshow.ts line 1: // fallow-ignore-file code-duplication — is "fallow" the project's lint-ignore directive? Just making sure this isn't a typo for "eslint-disable" or similar.
  • The parseTiming helper in the lint rule re-implements timing extraction that resolveTimeRange also handles. Not a problem now, but as the stack grows, consider whether the lint rule should delegate more to the parser rather than re-deriving scene data.

Verdict

This is a well-designed foundation. The schema is minimal and extensible, the parser handles edge cases thoughtfully (partial overrides, missing scenes), the lint rule integrates cleanly with the existing linter architecture, and test coverage is comprehensive (13 test cases covering happy paths and all error variants).

The isManifest shallow validation and the duplicated isSceneLikeCompositionId are the two items I'd want addressed before this merges — everything else is nits or "consider for a follow-up."

Not stamping — leaving for the author to address the above, then happy to re-review.

Review by Miga

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 778e53500acdeeb51fddfbd9f43f514127fede91.

Verdict: clean foundation design — schema/parser/resolver shape is right, test coverage on resolve paths is good. Layering onto Miga's review (who covered isManifest shallow validation, isSceneLikeCompositionId drift, multi-island silent ignore, fragment boundary, region bounds, TTS doc tags). A few things they didn't surface that matter for this being the base of a 10-PR stack.


🛑 Blockers

None.

⚠️ Concerns

1. Lint findings carry no location info — observability gap for the rule's whole job.
packages/core/src/lint/rules/slideshow.ts:55-78 — every finding the slideshow rule emits has only code + message + fixHint. No elementId, no selector, no snippet. Every other rule in packages/core/src/lint/rules/ populates at least one of these (composition.ts:128-136, core.ts, media.ts, gsap.ts, textures.ts all do). When a hyperframes lint user gets Slideshow manifest error: slide references unresolved sceneId "ghost", they don't know whether the offender is in the main-line slides array or in slideSequences[2].slides[4]. For a manifest with 20 slides across 3 sequences, this turns a one-line fix into a hunt. Suggest: thread a path-like locator (slides[3] / slideSequences["deep"].slides[1]) through resolveSlide errors, and populate the island's <script> tag location as snippet (extract via ctx.scripts.find(s => /type=["']application\/hyperframes-slideshow\+json["']/.test(s.attrs))).

2. Duplicate sequence id silently overwrites — wire-contract bug for hotspots.
parseSlideshow.ts:117-123for (const seq of manifest.slideSequences ?? []) { sequences[seq.id] = ... }. If two sequences share an id, the second wins, and any earlier hotspots that targeted that id now resolve to the wrong branch with no error. Since hotspot target → sequence is the only navigation primitive in this schema, a silent collision corrupts the deck. Three-line fix:

if (sequences[seq.id]) {
  errors.push(`duplicate slideSequence id "${seq.id}"`);
  continue;
}
sequences[seq.id] = { ... };

3. Lint rule re-parses ctx.source instead of using the centralized ctx.scripts extractor.
packages/core/src/lint/rules/slideshow.ts:53 calls parseSlideshowManifest(ctx.source), which builds its own regex against the full HTML source. But ctx.scripts already contains every <script> block extracted by the centralized SCRIPT_BLOCK_PATTERN in packages/core/src/lint/utils.ts:21. Two failure modes from running parallel extractors:

  • The two regexes can diverge subtly (the central one uses <script\b([^>]*)>, the new one uses <script[^>]*type=["']…["'][^>]*>). A script tag whose attribute is unquoted (type=application/hyperframes-slideshow+json) matches the central extractor but not the new one.
  • When Miga's "multiple islands" concern gets addressed, the right loop body is for (const block of ctx.scripts.filter(s => …)) — that's a free fix if you migrate to ctx.scripts now.
    Suggest: filter ctx.scripts by MIME type in the rule, JSON.parse the content, and drop the duplicate regex from parseSlideshowManifest entirely (or keep it as the non-lint extractor for direct consumers).

4. ISLAND_TYPE MIME constant duplicated across files.
parseSlideshow.ts:10 defines ISLAND_TYPE = "application/hyperframes-slideshow+json". The same string appears verbatim in lint/rules/core.ts:167 (the exemption regex), lint/rules/slideshow.ts:60 (the fix-hint message), all four test files (literal strings in HTML fixtures), and the file-level comment block. With 4 downstream PRs (player, studio, skill, examples) about to import this layer, the chance that someone tweaks the MIME type in one place and not another goes up. Suggest exporting ISLAND_TYPE from slideshow.types.ts (or a new slideshow/constants.ts) and importing it everywhere — including the regex literal in core.ts:167 (use new RegExp against the imported constant).

💡 Nits

5. Empty-slides manifest silently passes.
parseSlideshow.ts:125manifest.slides.map(...) = [], no errors. A manifest with {"slides": []} (or one whose only slides got removed) lints clean but is nonsense for a slideshow. Probably fine to leave for the player layer to handle ("0-slide composition = no slideshow surface"), but if resolveSlideshow.errors is meant to be the exhaustive validation gate before player accepts a manifest, worth a slides array is empty finding here.

6. Sequence slides aren't overlap-checked.
parseSlideshow.ts:138-145 overlap check applies only to slides (main-line), not slideSequences[*].slides. Reasonable — sequences are branches that may interleave with main-line time — but worth a one-line comment clarifying intent so the player author doesn't assume sequences are also non-overlapping.

7. // fallow-ignore-file code-duplication at slideshow.ts:1.
Effectively asking the repo's Fallow audit CI (which is passing) to skip flagging the isSceneLikeCompositionId copy. That's the right call short-term, but it means Miga's "SYNC comment" suggestion is doubly important — the fallow-ignore explicitly suppresses the one signal that would catch divergence.

✅ Looks good

  • The ./slideshow subpath export design — agreed with Miga, keeps player/studio bundles thin.
  • resolveTimeRange partial-override semantics + tests at parseSlideshow.test.ts:80-129 are exemplary; the "no scene + only one bound" error path is exactly the kind of nuance authors hit.
  • TTS reservation as parse-and-carry — defers cleanly without polluting the resolver.
  • core.ts:167 MIME exemption from inline-script-syntax check — necessary and minimal.
  • 13 tests covering happy path + every error variant. Solid.

Stack-coherence note

This is the foundation 4 downstream PRs import from. Concerns 1 (location info), 2 (sequence id collision), and 4 (MIME constant) all get harder to fix once player/studio start importing ResolvedSlideshow.sequences / parseSlideshowManifest. Cheap to address now, expensive later.

Not stamping — Miga's concerns + the above. Happy to re-review.

Review by Rames D Jusso

@vanceingalls vanceingalls merged commit 8376457 into main Jun 19, 2026
60 of 65 checks passed
@vanceingalls vanceingalls deleted the ss-core branch June 19, 2026 07:31
vanceingalls added a commit that referenced this pull request Jun 19, 2026
Addresses review feedback on the split stack:

- CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav
  buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once
  per document). No inline event handlers → works under strict CSP.
- IDs: studio sequence/hotspot id generation used Date.now() (sub-ms
  collision on rapid clicks) — now crypto.randomUUID().
- Versioning: stamp version on the persisted manifest island (preserving an
  existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION
  to the core schema so future schema changes can migrate older islands.

These live on the review-fixes tip (consistent with the stack's fixup-on-tip
model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b
(#1591/#1592), and ss-core (#1580).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Jun 19, 2026
- player: bundle @hyperframes/core into the IIFE/global build (noExternal)
- player: resolve audience mode from ?mode=audience URL query, not just attr
- player: event-driven waitForScenes + loud failure when no slides resolve
- player: scope window keydown so Space/Backspace don't hijack the host page
- player: audience mirrors full position (branch + fragment) via syncTo
- player: next() reveals remaining fragments even at slide end; enterBranch ignores empty sequences
- core: harden extractScenes against null/non-object scene entries
- core: strict manifest validation; error on inverted ranges & empty hotspot targets; dedup fragments
- core/lint: accept data-end/timeline-derived scene durations (match runtime)
- core+studio: share ISLAND_TYPE + island regex from @hyperframes/core/slideshow
- studio: SlideList reflects manifest slide order; branch-slide authoring (notes/fragments/hotspots)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Jun 19, 2026
Addresses review feedback on the split stack:

- CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav
  buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once
  per document). No inline event handlers → works under strict CSP.
- IDs: studio sequence/hotspot id generation used Date.now() (sub-ms
  collision on rapid clicks) — now crypto.randomUUID().
- Versioning: stamp version on the persisted manifest island (preserving an
  existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION
  to the core schema so future schema changes can migrate older islands.

These live on the review-fixes tip (consistent with the stack's fixup-on-tip
model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b
(#1591/#1592), and ss-core (#1580).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Jun 19, 2026
- player: bundle @hyperframes/core into the IIFE/global build (noExternal)
- player: resolve audience mode from ?mode=audience URL query, not just attr
- player: event-driven waitForScenes + loud failure when no slides resolve
- player: scope window keydown so Space/Backspace don't hijack the host page
- player: audience mirrors full position (branch + fragment) via syncTo
- player: next() reveals remaining fragments even at slide end; enterBranch ignores empty sequences
- core: harden extractScenes against null/non-object scene entries
- core: strict manifest validation; error on inverted ranges & empty hotspot targets; dedup fragments
- core/lint: accept data-end/timeline-derived scene durations (match runtime)
- core+studio: share ISLAND_TYPE + island regex from @hyperframes/core/slideshow
- studio: SlideList reflects manifest slide order; branch-slide authoring (notes/fragments/hotspots)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Jun 19, 2026
Addresses review feedback on the split stack:

- CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav
  buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once
  per document). No inline event handlers → works under strict CSP.
- IDs: studio sequence/hotspot id generation used Date.now() (sub-ms
  collision on rapid clicks) — now crypto.randomUUID().
- Versioning: stamp version on the persisted manifest island (preserving an
  existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION
  to the core schema so future schema changes can migrate older islands.

These live on the review-fixes tip (consistent with the stack's fixup-on-tip
model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b
(#1591/#1592), and ss-core (#1580).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Jun 19, 2026
* fix(slideshow): address code-review findings #1580-1584

- player: bundle @hyperframes/core into the IIFE/global build (noExternal)
- player: resolve audience mode from ?mode=audience URL query, not just attr
- player: event-driven waitForScenes + loud failure when no slides resolve
- player: scope window keydown so Space/Backspace don't hijack the host page
- player: audience mirrors full position (branch + fragment) via syncTo
- player: next() reveals remaining fragments even at slide end; enterBranch ignores empty sequences
- core: harden extractScenes against null/non-object scene entries
- core: strict manifest validation; error on inverted ranges & empty hotspot targets; dedup fragments
- core/lint: accept data-end/timeline-derived scene durations (match runtime)
- core+studio: share ISLAND_TYPE + island regex from @hyperframes/core/slideshow
- studio: SlideList reflects manifest slide order; branch-slide authoring (notes/fragments/hotspots)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(player): slideshow fullscreen + presenter-view rework

- fullscreen toggle in the nav chrome (button + 'F' key); standard Fullscreen
  API on the <hyperframes-slideshow> element, icon reflects state
- presenter console: live slide on top, speaker-notes panel below, with the nav
  controls shown in-view; Present button hides once presenting (harness)
- audience (viewer) window: chrome reduced to a fullscreen-only control, no nav
- fix: audience / back() / backToMain() mirror stayed frozen on the first frame —
  a bare paused seek does not repaint some compositions. resumeSlide now plays a
  brief render-nudge (RENDER_NUDGE) past the target so the composition paints,
  then onTime pauses at the hold
- refactor: extract reusable buildNavCluster() + wireChromeButtons(); rework
  buildPresenterLayout into the bottom notes panel
- example: airbnb-deck presenter-test.html harness (Present button + 'F')

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(player): slideshow no auto-progress + presenter slide fits/pins

- navigation jumps to a static frame instead of auto-playing the timeline:
  playTo() seeks to the hold (+ a brief RENDER_NUDGE to repaint) rather than
  sustaining playback, so slides hold until the user advances
- presenter view: pin the live slide to the top and confine the player to the
  region above the notes panel, so the player CONTAINS the composition — the
  full slide stays visible (letterboxed) at any width and re-fits on resize;
  its bottom is no longer cut off by the notes panel
- tests: seek targets updated for the render-nudge offset

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(slideshow): presenter nav flash, slide-1 boundary, branch buttons

Three presenter-mode fixes from testing the airbnb deck: (1) navigation flash — seek to the exact target then play forward to repaint, instead of seeking backward (t-0.2) which painted the previous scene at boundaries; split hold into holdTarget (logical) and holdAt (target+nudge, clamped to slide.end). (2) slide-1 boundary — no-fragment slides rest at the slide midpoint, not slide.end. (3) presenter branch buttons — surface hotspots as buttons in the presenter console (the on-slide pill is lost in the letterboxed view). Also extract paintChrome() to dedupe the three chrome-render sites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(slideshow): stop presenter nav buttons flickering / dropping clicks

The presenter elapsed clock called render() every second, which rebuilt the
entire chrome (innerHTML) including the nav buttons — they flickered and any
click landing mid-rebuild was lost. The 1s tick now updates only the elapsed
text node; the nav buttons are rebuilt only on actual navigation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(slideshow): CSP-safe nav hover, UUID editor ids, manifest version

Addresses review feedback on the split stack:

- CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav
  buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once
  per document). No inline event handlers → works under strict CSP.
- IDs: studio sequence/hotspot id generation used Date.now() (sub-ms
  collision on rapid clicks) — now crypto.randomUUID().
- Versioning: stamp version on the persisted manifest island (preserving an
  existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION
  to the core schema so future schema changes can migrate older islands.

These live on the review-fixes tip (consistent with the stack's fixup-on-tip
model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b
(#1591/#1592), and ss-core (#1580).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(ci): fix format + fallow gates for slideshow stack

- .prettierignore: exclude generated demo compositions (registry/examples/**/*.html)
  from oxfmt — large video-pipeline output (GSAP/Three/WebGL), not hand-authored
  source. Was failing 'Format' repo-wide (pre-existing on main via #1584).
- .fallowrc: exempt SlideshowPanel.tsx (health/complexity — section fan-out) and
  the slideshowPanelHelpers.ts / SlideshowPanel.test.ts parallel-structure clones
  (duplicates.ignore). File-level config, not inline comments — inline shifts line
  numbers and breaks fallow's inherited-finding fingerprint (per existing rc note).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(slideshow): address PR review + CodeQL findings

- CodeQL #638 (parseSlideshow): complete the regex metachar escape in
  slideshowIslandRegex (was missing backslash); add JSDoc on the factory +
  lastIndex caveat (reviewer 5a/16).
- CodeQL #639/#640 + review items 13/17: remove registry/examples/airbnb-deck/
  presenter-test.html — a generated test harness (postMessage w/o origin check,
  proto-pollution) that was scope-creep into a fix PR and a 3rd duplicate island.
  Regenerate locally via the scratchpad script when testing.
- Review item 15 (docs drift in skills/slideshow/SKILL.md): lint resolves scenes
  by data-composition-id only (not .clip[id]); fragments are valid INCLUSIVE of
  [start,end], not 'strictly inside'.

IIFE bundles core confirmed (0 external @hyperframes/core refs in the slideshow
global build). format/lint/fallow green.

* feat(cli): add 'present' command — serve a deck in presenter mode

hyperframes present [dir] starts a lightweight HTTP server, wraps the
composition in <hyperframes-slideshow> with its island inlined, and opens
the browser. A real HTTP origin is required for presenter mode: present()
opens the audience window via window.open(?mode=audience) and the two sync
over BroadcastChannel — neither works from file://.

- New utils/compositionServer.ts factors the server scaffolding shared with
  'play' (resolve runtime/player/slideshow bundles, inject runtime, asset
  content-types, bind to a free port); play.ts now uses it too.
- Errors clearly if the deck has no slideshow island.
- .fallowrc: exempt the play/present command entrypoints (validation + server
  wiring) and the per-command startup/logging block from the complexity /
  duplication gates.

Verified end-to-end against registry/examples/airbnb-deck: server serves the
wrapper + assets, the component binds and renders (counter 1 / 11).

* fix(cli): present renders the deck (player sizing + self-driving serve)

Two bugs caused a black slide area:
- The <hyperframes-player> had no positioning, so its iframe collapsed to
  zero size — the (absolutely-positioned) chrome showed but the composition
  didn't. Add position:absolute; inset:0 (matches demo.html).
- The composition was served with the engine runtime injected, which leaves
  its timelines engine-paused (blank). Slideshow decks self-drive their own
  timelines (like demo.html / the standalone harness), so serve them raw.

Verified end-to-end on registry/examples/airbnb-deck: cover renders, Next
advances 1/11 -> 2/11 and slide 2 paints.

* fix(cli): present plays slideshow sound effects

The composition (in the player's sandboxed iframe) posts
{ type: 'hf-sfx', name } to the parent on nav, but the iframe is
autoplay-blocked — audio must play in the parent that owns the user gesture.
Add the parent-side hf-sfx handler (the 4 standard clips advance/fragment/
branch-enter/back, served from the deck's sfx/ under /composition/sfx/),
gesture-unlocked and mute-aware, in both presenter and audience windows.

Verified: sfx serve 200 (audio/mpeg) and Next delivers [advance, fragment]
to the parent handler.

* feat(examples): softer mellow slideshow sfx for airbnb-deck

Replace the aggressive percussive pops with gentle sine-tone cues (warm
pitches C5/G4/E5/F4, 12ms attack + exponential decay, lowpassed) — advance/
fragment/branch-enter/back. Much lighter; fragment is the most subtle.

* feat(examples): whoosh + sparkle slideshow sfx for airbnb-deck

Replace the sine-tone cues with airy, designed sounds:
- advance: a soft whoosh (band-limited pink noise, bell-shaped swell)
- back: that whoosh reversed and darkened
- fragment: a light sparkle (staggered high chime blips)
- branch-enter: whoosh + a trailing sparkle (magical entry)

* feat(examples): directional whoosh + richer branch-enter cue (airbnb-deck)

- Going backward a slide now plays the reverse whoosh (back), not advance —
  the sfx logic detects nav direction by scene order instead of firing advance
  for every scene change.
- branch-enter is now a more interesting magical cue: a faint whoosh + an
  ascending C5-E5-G5-C6 chime arpeggio + a trailing sparkle.

Verified: next then prev fires [advance, fragment, back]; no page errors.

* fix(cli): harden present sfx handler + mute-hover affordance (R2 review)

Addresses Rames R2 items 19-21:
- 20: the present audio handler reintroduced the CodeQL classes removed with
  presenter-test.html — add an origin check (same-origin composition iframe)
  and an own-property guard so a 'name' like __proto__ can't resolve to and
  mutate Object.prototype.
- 21: assetContentType used a bare index lookup (ext='__proto__' -> prototype);
  guard with Object.hasOwn.
- 19: the CSP hover rule erased the speaker button's muted color; add a
  higher-specificity [data-hf-muted] [data-hf-mute]:hover override.

Verified: hf-sfx origin matches location.origin (guard passes), advance/fragment
still fire, deck renders + advances. Items 14/18/22 deferred (minor, pre-existing).

* fix(slideshow): address remaining R2 items (14/18/22) + re-remove harness

- 14: resumeSlide now mirrors enterSlide — a no-fragment slide resumes at its
  midpoint (visible-at-rest), not frame-0; fragmented slides still resume to the
  saved fragment or slide.start. Added a dedicated test naming the heuristic.
- 18: fullscreenchange swaps only the fullscreen glyph + aria (hoisted SVGs to
  module consts) instead of re-rendering the whole chrome.
- 22: .prettierignore lists the specific generated demo compositions instead of
  blanket registry/examples/**/*.html, so hand-authored example HTML still formats.
- presenter-test.html: a stray 54a4460 git add -A had re-added the deleted
  harness (reviving CodeQL #639/#640); remove it again.

106 slideshow tests pass; tsc/lint/fallow/format clean; deck still renders.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review: checking prior findings

Re-reviewing against 778e535 — the same (and only) commit. No follow-up pushes since the original reviews from Miga and Rames.

Status of previously flagged items

1. Shallow isManifest() validation — STILL OPEN

parseSlideshow.ts isManifest() still only checks typeof === "object" and Array.isArray(slides). A manifest like {"slides": [42, null, "oops"]} passes validation and then blows up inside resolveSlide with an unhelpful error (trying to read .sceneId off a number). Minimum fix: slides.every(s => typeof s?.sceneId === 'string').

2. Duplicated isSceneLikeCompositionId — STILL OPEN

lint/rules/slideshow.ts still contains a full copy of isSceneLikeCompositionId from runtime/timeline.ts. The comment says "Mirrors" but there's no sync mechanism — no import, no // SYNC: marker, no shared util. The fallow-ignore-file code-duplication directive suppresses the one CI signal that would catch divergence. At minimum, add a // SYNC: runtime/timeline.ts:isSceneLikeCompositionId comment so grep can find both copies. Extracting to a shared util would be better, especially with 4 downstream PRs about to build on this.

3. Sequence-id collision — STILL OPEN (originally flagged by Rames)

parseSlideshow.ts:117-123 still does sequences[seq.id] = { ... } with no collision check. Duplicate sequence IDs silently overwrite earlier entries, which means hotspots targeting that ID resolve to the wrong branch with no error. Three-line fix:

if (sequences[seq.id]) {
  errors.push(`duplicate slideSequence id "${seq.id}"`);
  continue;
}

Summary

All three items from the prior round remain unaddressed — no new commits have been pushed since the original reviews. Waiting on a follow-up push before re-evaluating.

Re-review by Miga

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants