Skip to content

Add experimental Presentation API presenter plugin#1

Draft
LeaVerou wants to merge 3 commits into
mainfrom
claude/serene-edison-2xrv9
Draft

Add experimental Presentation API presenter plugin#1
LeaVerou wants to merge 3 commits into
mainfrom
claude/serene-edison-2xrv9

Conversation

@LeaVerou

@LeaVerou LeaVerou commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces a new experimental presenter plugin (presenter2) built on the W3C Presentation API as an alternative to the classic window.open-based presenter. This allows presenters to render slides on secondary displays or Cast devices with automatic reconnection support.

Key Changes

  • New presenter2/plugin.js: Experimental presenter plugin using the Presentation API instead of window.open(). Features include:

    • Controller/receiver pattern for presenter and audience views
    • Message-based synchronization of slide and item navigation
    • Automatic reconnection when presenter view is reloaded via sessionStorage
    • Ctrl+P keyboard shortcut to initiate presentation
    • Graceful fallback for unsupported browsers
  • New presenter/presenter-ui.js: Extracted transport-independent presenter UI logic shared between classic and experimental plugins:

    • enterPresenterView(): Initializes presenter view styling and opens speaker notes
    • onPresenterSlidechange(): Handles speaker notes display and timing calculations
    • Works with both window.open and Presentation API transports
  • Updated presenter/plugin.js: Refactored to use shared UI functions from presenter-ui.js, reducing code duplication

  • Updated plugin-autoload.js: Added conditional autoloading:

    • Classic presenter plugin loads when details.notes exist and experimental-presentation-api class is absent
    • New presenter2 plugin loads when details.notes exist and experimental-presentation-api class is present
    • Ensures only one presenter plugin runs per deck
  • Updated details-notes/plugin.js: Extended presenter detection to recognize both Inspire.projector and the presenter class on body

  • New presenter2/README.md: Documentation covering usage, benefits, requirements, and limitations

  • New presenter2/plugin.css: Stylesheet that reuses classic presenter styling via import

Implementation Details

  • Uses PresentationRequest API for display selection and PresentationConnection for bidirectional messaging
  • Implements a guard (applyingRemote) to prevent echoing remote updates back to the other view
  • Stores presentation connection ID in sessionStorage to enable silent reconnection after reload
  • Syncs only slide/item navigation (via slide IDs), not keyboard/mouse events
  • Requires secure context (HTTPS or localhost) and Chromium-based browser support

https://claude.ai/code/session_01PLrW3z9316nvWakLoqYaY5

Introduces an opt-in experimental presenter plugin that uses the W3C
Presentation API instead of window.open(), letting the browser render the
audience view on a secondary display or Cast device and syncing the two
views over a PresentationConnection. Includes silent reconnect of the
presenter view after a reload via PresentationRequest.reconnect() + a
sessionStorage-persisted presentation id.

Enable per-deck with the `experimental-presentation-api` body class, which
also disables the classic presenter plugin so the two never load together;
without the class the classic window.open presenter remains the fallback.

The transport-independent presenter UI (notes, next-slide preview, timing)
is extracted into a shared presenter/presenter-ui.js module that both the
classic and experimental plugins import, keeping behavior identical.

@DmitrySharabin DmitrySharabin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review

The core design is sound — I verified against @inspirejs/core that:

  • goto() accepts bare string slide ids ✓
  • slidechange hook fires synchronously inside goto(), so the applyingRemote echo-suppression guard works ✓
  • All slides get auto-generated ids in init(), so Inspire.currentSlide.id is always a non-empty string ✓
  • gotoItem also fires its hook synchronously, so item sync is echo-safe too ✓

The shared presenter-ui.js extraction is clean, and the autoload selector trick is clever.

Two issues worth discussing before merging, one functional and one dead-code.

Comment thread presenter2/plugin.js
console.warn(
"[presenter2] The Presentation API is not supported in this browser. " +
"Remove the `experimental-presentation-api` class to use the classic presenter.",
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: cancelled picker leaves presenter view stuck

enterPresenterView() runs on line 115 before start() resolves. If the user cancels the display picker (or no display is available), the .catch(() => {}) on line 122 silently swallows the rejection — but the body already has presenter + show-next classes and notes are open, with no audience view and no way to revert short of reloading.

The same gap exists on disconnect: drop() (line 72) nulls transport and clears sessionStorage, but never reverts the presenter UI.

Suggested fix — move enterPresenterView() into the .then() (like the reconnect path already does correctly on line 100), and add a leavePresenterView() counterpart called from drop() when isController:

// keyup handler
new PresentationRequest([location.href]).start()
    .then(connection => {
        enterPresenterView();
        wireConnection(connection, true);
        window.focus();
    })
    .catch(() => {}); // picker cancelled — no UI change needed now

For drop(), something like:

let drop = () => {
    transport = null;
    if (isController) {
        sessionStorage.removeItem(STORAGE_KEY);
        document.body.classList.remove("presenter", "show-next");
    }
};

Comment thread details-notes/plugin.js
}

if (Inspire.projector) {
if (Inspire.projector || document.body.classList.contains("presenter")) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Likely dead code — this check can never be true at evaluation time

This module runs at import time (top-level for loop, no hooks). At that point, neither Inspire.projector nor the presenter body class is set — both are added later by user interaction (Ctrl+P) or by the init-end hook (reconnect path).

The original Inspire.projector check had the same problem, so this isn't a regression — but adding a second never-true condition makes it look intentional. In practice, notes are opened by enterPresenterView() and onPresenterSlidechange() in the presenter plugins, so this line is redundant.

Worth either removing the whole if block (since it's always false) or, if the intent is to support late-loaded plugins where the class might already be set, adding a comment explaining the scenario.

DmitrySharabin and others added 2 commits June 6, 2026 18:06
- Move enterPresenterView() into start().then() so cancelling the
  display picker doesn't leave the page in presenter mode
- Tear down presenter classes on connection close/terminate

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

3 participants