Skip to content

Add --bespoke.thumbnails option for slide thumbnails in presenter view#705

Closed
mil-ad wants to merge 11 commits intomarp-team:mainfrom
mil-ad:slide-navigator
Closed

Add --bespoke.thumbnails option for slide thumbnails in presenter view#705
mil-ad wants to merge 11 commits intomarp-team:mainfrom
mil-ad:slide-navigator

Conversation

@mil-ad
Copy link

@mil-ad mil-ad commented Feb 26, 2026

Adds an optional --bespoke.thumbnails CLI flag for showing thumbnails of all slides in the presenter view. This allows the presenter to jump directly to any slide without cycling through intermediate slides in front of the audience:

screen-area-capture-2026-02-26-18.11.25.mp4

Usage

Off by default and can be enabled via CLI:

marp --bespoke.thumbnails --preview slides.md

Or via config file:

{ "bespoke": { "thumbnails": true } }

I started by having iframes but that that proved too slow so moved to cloning each slide's SVG into a scrollable thumbnail strip on the right panel.

@mil-ad
Copy link
Author

mil-ad commented Feb 26, 2026

closes #464

@yhatt
Copy link
Member

yhatt commented Feb 27, 2026

Regarding #464, we imagine it more like a Reveal.js screen with the popup modal (shows ?view=overview), rather than a thumbnail list in a small area at the top right. I feel it should be more efficient, because it only uses one iframe, and already present slides in DOM can re-use. If that were such a feature, there would be little need to add a toggle flag to the CLI. What do you think?

@mil-ad
Copy link
Author

mil-ad commented Feb 27, 2026

Ah fair enough. I updated it:

screen-area-capture-2026-02-27-12.30.27.mp4

Is this more aligned with you have in mind?

also - apologies for the potential AI slop. I had to get some help. I'm not a js guy.

@mil-ad
Copy link
Author

mil-ad commented Feb 27, 2026

One question though. Do we want this command available from the normal view as well or just the presenter view?

@yhatt
Copy link
Member

yhatt commented Feb 28, 2026

Oh yes, based on the original request marp-team/marp#326, it would be good if it is available in either mode.

I’ve updated the main branch in #706 to fix the blocked CI. Could you pull in the latest changes?

@mil-ad mil-ad force-pushed the slide-navigator branch from 8b5749a to 6113c10 Compare March 1, 2026 09:34
@mil-ad
Copy link
Author

mil-ad commented Mar 1, 2026

Could you pull in the latest changes?

Done

@yhatt yhatt self-requested a review March 2, 2026 12:36
@mil-ad
Copy link
Author

mil-ad commented Mar 10, 2026

@yhatt do you see this getting into main or is it too hacky?

@yhatt
Copy link
Member

yhatt commented Mar 11, 2026

For maintainability, I've expected the overview mode to be splitted from regular view modes just like ?view=overview, similar to other view modes ?view=presenter.

If so, the overview modal in both of regular mode and presenter mode only needs to display a URL with its mode rewritten to overview within <iframe>, and the changed state of page would be automatically synced to all opening windows powered by bespokeSync plugin. In addition, the user also can open only the overview mode by setting the query of URL to ?view=overview manually.

@mil-ad
Copy link
Author

mil-ad commented Mar 13, 2026

Ok I think I have changed it based on your feedback. Let me know what you think.

One thing I'm not sure about is whether the overview view should open in a new window or not. It currently does open in a new windows.

mil-ad added 11 commits March 13, 2026 14:26
The previous presenter view loaded a full copy of the presentation in an
iframe to show a single next-slide preview. Replace this with SVG cloning
that copies already-rendered slide nodes from the DOM, enabling instant
thumbnails of all slides with click-to-navigate.

The feature is opt-in via --bespoke.thumbnails (or config file), following
the existing --bespoke.osc/progress/transition pattern. When disabled
(default), the original iframe-based next-slide preview is preserved.
Remove the --bespoke.thumbnails CLI option and presenter view thumbnail
panel. Add a new overview plugin that displays all slides in a
CSS-transform-based grid, toggled with 'o' or Escape in both normal and
presenter views. Arrow keys navigate focus, Enter/click selects a slide.
Instead of an overlay within normal/presenter views, overview is now a
separate view mode following the same pattern as ?view=presenter. Pressing
'o' opens a new synced window, and navigation syncs automatically via
bespokeSync.
Pressing 'o' now navigates the current window to ?view=overview.
From overview, clicking a slide or pressing Escape/Enter/o navigates
back to normal view at that slide. Also fixes hash duplication bug
in navigateBack.
Copy link
Member

@yhatt yhatt left a comment

Choose a reason for hiding this comment

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

I've made a follow-up implementation at #710. Please refer that how to fix them:

Comment on lines +30 to +39
function openOverviewView(this: BespokeForOverview) {
const { max, floor } = Math
const w = max(floor(window.innerWidth * 0.85), 640)
const h = max(floor(window.innerHeight * 0.85), 360)

return window.open(
this.overviewUrl,
overviewPrefix + this.syncKey,
`width=${w},height=${h},menubar=no,toolbar=no`
)
Copy link
Member

Choose a reason for hiding this comment

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

I think it's better to be overlay on the current view instead of opening new window.

Comment on lines +13 to +38
const applyLayout = () => {
const cols = columns()
const rows = Math.ceil(slideCount() / cols)
const parent = deck.parent as HTMLElement
const rect = parent.getBoundingClientRect()
const gap = 20
const cellW = (rect.width - gap * (cols + 1)) / cols
const cellH = (rect.height - gap * (rows + 1)) / rows

const scaleX = cellW / rect.width
const scaleY = cellH / rect.height
const cellScale = Math.min(scaleX, scaleY)

const renderedW = rect.width * cellScale
const renderedH = rect.height * cellScale

slides().forEach((slide, i) => {
const col = i % cols
const row = Math.floor(i / cols)
const x = gap + col * (renderedW + gap)
const y = gap + row * (renderedH + gap)

slide.style.transformOrigin = '0 0'
slide.style.transform = `translate(${x}px, ${y}px) scale(${cellScale})`
})
}
Copy link
Member

Choose a reason for hiding this comment

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

Use CSS Grid to make simpler JS.

Comment on lines +48 to +50
slide.addEventListener('click', () => {
deck.slide(i)
})
Copy link
Member

Choose a reason for hiding this comment

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

a11y: Each slide should be focusable by setting tabindex="0", and navigatable by hitting Entry or Space in addition to the click event.

@mil-ad
Copy link
Author

mil-ad commented Mar 13, 2026

I've made a follow-up implementation at #710. Please refer that how to fix them

Amazing. We can close this one then?

@yhatt
Copy link
Member

yhatt commented Mar 13, 2026

Yes. I'll credit you in the CHANGELOG. The feature request wouldn't have been resolved without your contribution 😎

@mil-ad
Copy link
Author

mil-ad commented Mar 14, 2026

🫡

@yhatt
Copy link
Member

yhatt commented Mar 14, 2026

Shipped as v4.3.0 🚢

@yhatt yhatt closed this Mar 14, 2026
@arseru
Copy link

arseru commented Mar 15, 2026

Thank you both for working on this, I'm very happy to see it merged! :)

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