Build a Nostr-native presentation system on top of the existing canvas
editor:
- Slide (templateMode 'slide') — a 16:9 Custom Art canvas with speaker
notes; publishes/forks/loads as casewrap-slide.
- Deck Builder (templateMode 'deck') — a Template-tab container with an
inline ordered slide list. Slide-sorter view: add blank, import from
Nostr or file (non-16:9 imports letterboxed), duplicate, delete,
drag-reorder, inline-rename; edit a slide in the canvas editor with
write-back. Self-contained save/load/publish/fork as casewrap-deck;
deck feed-card and publish previews show the first slide + count badge.
- Deck theme — deck-level font + background, render-time composition,
per-slide ignoreDeckTheme opt-out.
- Presenter Mode — full-screen runtime: themed slide, notes, next
thumbnail, counter; keyboard/click/on-screen nav; Presenter ⇄ Audience
view toggle (slide-only full-screen with auto-hiding chrome). Launches
from the deck tool palette and a deck's community-browser preview.
Supporting changes:
- New categories: slide ("Slide design") and slide-deck ("Slide deck");
both use the free-form Custom tag field, not media identifiers.
- Swap the Template/Designer tab icons; 50% larger.
- Remove the IMDb/OMDb metadata import (button, handler, omdbApiKey);
the imdbId field stays.
- Update README, SLIDE_DECK_FEATURE.md status, and TODO.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
23 KiB
Slide Decks & Presenter Mode
Goals
Add a presentation system to Artstr Studio: design individual slides, assemble them into an ordered deck, and run a presenter mode to show the deck — all Nostr-native, forkable, and driven entirely off JSON data.
Product goals
- A Slide Designer — a 16:9 design surface (a flat canvas, like Custom Art) with an added speaker-notes field. A slide is a first-class Nostr design: publishable, forkable, loadable.
- A Deck Builder — a container that holds an ordered, variable-length list of slides. The deck embeds its slides' full data inline, so it is a single self-contained file. Slides can be imported from published designs and extracted back out as standalone designs.
- A deck-level theme that overrides per-slide styling (fonts, background) across the whole deck — with a per-slide opt-out so an individual slide can keep its own look.
- A Presenter Mode that plays the deck full-screen with speaker notes and slide navigation.
Non-goals (v1)
- Dual-window presenter view (audience window + a separate presenter window with notes on a second screen). This is the real presenting experience and is the explicit Phase E follow-up — v1 is single-window.
- Slide transitions / animations / builds (step-by-step reveals).
- A presenter timer / rehearsal stats / laser pointer. Phase E+.
- Rich-text speaker notes — plain text for v1.
- Importing PowerPoint / Keynote / PDF decks.
- Exporting a deck to PDF or slideshow video. A natural future ask, but not v1 — Presenter Mode is the v1 way to show a deck. (Individual slides still export as images like any Custom Art design.)
- A per-property theme matrix — v1 theme overrides a small fixed set (font family, background); per-property locking is future polish.
Implementation status
- Phase A — Slide format: done.
templateMode: 'slide'(a thin alias of the Custom Art canvas engine viaisCanvasMode()), Slide Designer layout on the Designer tab, 16:9-locked Canvas panel + Speaker notes textarea (state.customArt.notes), andslidesave / load / publish / fork ascasewrap-slide. Slides appear in the community browser. - Phase B — Deck Builder: done.
templateMode: 'deck'with the embedded-slidedeckdata model; Deck Builder layout on the Template tab; the slide-sorter view (thumbnail grid with add-blank / import from Nostr / import from file / duplicate / delete / drag-reorder); open a slide in the canvas editor and write back viapersistEditingDeckSlide(); deck save / load / publish / fork ascasewrap-deck, self-contained; deck feed-card and publish-confirm previews show the first slide with a slide-count badge. Import letterboxes non-16:9 sources onto the slide. - Phase C — Deck theme: done.
deck.theme(font family + optional background-color override) with controls in the Deck Builder sidebar; per-slideignoreDeckThemeopt-out in the slide editor; render-time theme composition (composeSlideForDeck) applied non-destructively to deck thumbnails and the community-browser preview. - Phase D — Presenter Mode (single-window): done. Full-screen runtime
(
#presenterMode): the themed current slide, speaker notes, a next-slide thumbnail, and an / totalcounter. On-screen prev/next, click-to- advance, and keyboard nav (arrows / space / PageUp-Down / Home / End / Esc); a Presenter-view ⇄ Audience-view toggle (Audience view shows the slide alone, full-screen, with auto-hiding chrome — for single-screen use). Presenter owns the keyboard via a capture-phase handler, so editor shortcuts are suppressed while it runs. Launches from a ▶ button in the Deck Builder tool palette and a "▶ Start Presentation" button on a deck's community-browser preview. - Phase E (dual-window presenter): not started — explicit future work.
The v1 plan (Phases A–D) is complete and shipping. Two new categories —
slide ("Slide design") and slide-deck ("Slide deck") — were added so
slides and decks classify on their own terms, and both use the free-form
"Custom tag" field rather than the media-identifier panel.
Remaining work
- Phase E — dual-window presenter (audience window + a separate presenter window with notes/timer on a second screen).
- v1 non-goals still deferred: slide transitions/animations, a presenter timer / rehearsal stats / laser pointer, rich-text speaker notes, PowerPoint/Keynote/PDF import, deck → PDF/video export, per-property theme locking, and a Keynote-style always-on filmstrip.
- Minor polish: the "Back to deck" bar is absolutely positioned (scrolls away if the slide-edit canvas overflows); deck theme background is color-only (an image-URL background was floated as future).
Why this fits the repo
This is the disc pattern generalized. A disc-label sheet
(templateMode: 'disc') embeds the disc designs assigned to its slots and
is self-contained for printing; disc designs (appMode: 'designer') are
also independently publishable and importable. A deck/slide pair works the
same way:
- Slide ⇄ disc design — a standalone, publishable design.
- Deck ⇄ disc-label sheet — a Template-tab container that embeds the designs it holds.
Everything a deck needs already exists in some form:
- The canvas editor edits a slide unchanged — a slide is mechanically a Custom Art canvas locked to 16:9. Layers, text, shapes, the pen tool, undo/redo, fit-to-view all apply for free.
- The persist/load-on-switch pattern (
persistCurrentDiscDesign/loadCurrentDiscDesignLayers) is exactly how editing one slide inside a deck will work — swap the editor'sstate.layersfor the active slide, write back on switch. - The import-a-design-into-a-slot flow (disc design → disc slot) is the model for "add a published design as a slide."
- Publish / fork / community browser / Lightning all reuse as-is.
The genuinely new surfaces are the slide sorter (the Deck Builder view) and the presenter runtime.
Current-state facts the plan is built on
appMode:'template' | 'designer'.templateMode:'cover' | 'disc' | 'jewel' | 'customart'.- The Designer tab is shown when
appMode === 'designer'ortemplateMode === 'customart'. The Template tab otherwise. - Each tab has a Layout panel:
layoutFieldset(Template — Case cover / Disc labels) anddesignerLayoutFieldset(Designer — Custom Art / Disc Design).applyLayout()maps a layout choice to the rightappMode/templateModeand crosses app modes when needed. - Custom Art canvas state:
state.customArt = { width, height, background, preset }, rendered on#sheet; payload viacustomArtPayload(). - Designs publish as kind-30078 parameterized replaceable events tagged
casewrap-<mode>, with thed-tag mode-prefixed. - Save discriminator (
templateModein payloads):cover,disc,jewel,customart,disc-design. - All image references are remote URLs — never embedded binary. A deck is therefore N slides of layer JSON, not megabytes of image data.
Framing within Template / Designer
Per the decision to keep the existing tab framing (even though a deck is a sequence, not a print artboard — the distinction stays intentionally muddy for now):
- Slide Designer — a new layout option in the Designer tab Layout
panel, beside Custom Art and Disc Design. Runs as
appMode: 'template', templateMode: 'slide'. The Designer-tab visibility test extends to… || templateMode === 'slide'. - Deck Builder — a new layout option in the Template tab Layout
panel, beside Case cover and Disc labels. Runs as
appMode: 'template', templateMode: 'deck'.
This mirrors disc design (Designer) vs disc-label sheet (Template): the standalone authored thing lives on the Designer side, the container that holds many of them lives on the Template side.
Two new templateMode values — slide and deck — are added to
applyLayout(), WORKFLOW_ORDER, the layout panels, the body-class /
visibility logic, and the save discriminator set.
Data model
Slide design (standalone — templateMode: 'slide')
{
version: SCHEMA_VERSION,
templateMode: 'slide',
templateType: 'slide',
meta: { … }, // title / category / language, as usual
slide: {
width: 1920, height: 1080, // 16:9, fixed aspect
background: '#ffffff',
notes: '' // plain-text speaker notes
},
layers: [ … ] // standard layer array
}
A slide is a Custom Art canvas with a slide object instead of customArt,
the addition being notes. Rendered on #sheet. The aspect is locked 16:9
(width/height adjustable for resolution, but the ratio holds).
Deck (templateMode: 'deck')
{
version: SCHEMA_VERSION,
templateMode: 'deck',
templateType: 'deck',
meta: { … }, // deck title / category / language
deck: {
theme: {
fontFamily: '', // '' = no override
background: '', // '' = no override (color or image URL)
// small fixed set for v1; extensible
},
slides: [
{
name: 'Intro', // optional label shown in the sorter
slide: { width, height, background, notes },
layers: [ … ],
ignoreDeckTheme: false // true = this slide keeps its own look
},
… // ordered, variable length
]
}
}
- The deck embeds each slide's full data inline — it is one self-contained file. Presenter mode and previews never depend on N external events resolving.
- A slide object inside
deck.slides[]is the same shape as a standalone slide design's{ slide, layers }, plus an optionalnameandignoreDeckTheme. Embedded slides carry nometaof their own — the deck'smetacovers the whole deck. - Import copies a published design's
{ slide?/customArt, layers }into a newdeck.slides[]entry (see surface #2 for cross-aspect handling). Extract lifts one entry back out into a standalone slide design for separate publishing.
Nostr
- Slide: kind-30078, tag
casewrap-slide,d-tagslide:<title|id>. - Deck: kind-30078, tag
casewrap-deck,d-tagdeck:<title|id>. - Both are forkable and editable-in-place via the existing NIP-33 flow.
- Schema version bumps;
slide/deck/notes/ignoreDeckThemeare additive — old clients ignore unknown payloads.
The surfaces
1. Slide editor
The existing canvas editor, opened on a 16:9 slide. The Designer-tab Layout
panel's "Slide Designer" option gives a Canvas-style panel (resolution
presets — 1920×1080, 1280×720 — background) plus a "Speaker notes"
textarea. Notes save into slide.notes. Nothing else changes — it is the
Custom Art editor with a locked aspect and one extra field.
When a slide is opened from within a deck, the panel shows a small notice that the deck theme will restyle fonts/background in the deck and presenter views — so the un-themed editor view isn't a surprise.
2. Deck Builder (the slide sorter)
A new view for templateMode === 'deck'. The canvas area shows a
reorderable grid of slide thumbnails; the sidebar holds deck title and
theme controls.
- Add slide — new blank slide · import from a published design · import from the community browser. Imports land on a 16:9 slide: a 16:9 source (slide / Custom Art) fills it; any other aspect (cover, square, …) is scaled to fit and centered, letterboxed.
- Reorder — drag thumbnails.
- Duplicate / Delete a slide.
- Edit a slide — clicking a thumbnail opens it in the canvas editor
(surface #1). The deck tracks
state.deck.editingIndex; entering edit loads that slide into editor state, and a "Back to deck" control persists it back — thepersistCurrentDiscDesign/loadCurrentDiscDesignLayerspattern. The active slide is persisted before any save, publish, reorder, or present. - Thumbnails are mini-renders via the existing slide preview-DOM
renderer +
fitMiniPreview. Only the edited slide's thumbnail re-renders on write-back — not the whole grid. - Canvas chrome — in the sorter view the bottom zoom bar and the right tool-options panel are hidden (nothing to zoom or configure); the left tool palette shows just the Present (▶) action button (see surface #3). Full chrome returns when a slide is opened in the canvas editor.
- Undo uses the app's single history stack: sorter operations (add / delete / reorder) and slide edits are ordinary entries; undoing an entry made in the other view switches to that view first.
- The sorter is its own view; a Keynote-style always-visible filmstrip alongside the canvas is deferred polish.
3. Presenter Mode (v1 — single window)
Entry points. Presenter Mode launches from a Present button — a play
glyph (▶) — used consistently in two places:
- In the editor — a button in the left tool palette, in the slot where the drawing tools normally sit. It is shown whenever a deck is active (the Deck Builder sorter, and while editing one of the deck's slides), so you can jump straight to presenting. It is an action, not a tool mode.
- In the template browser — a Start Presentation button on a deck's
full preview page (
openDesignPreview), beside Use/Fork · Share · Save JSON, using the same▶icon. This lets anyone present a published deck straight from the browser without forking it first.
Both entry points run the same full-screen runtime (Fullscreen API):
- The current slide is drawn with the existing slide / Custom-Art preview-DOM renderer, scaled to fill the viewport (letterboxed to 16:9) — no new slide renderer is needed.
- Speaker notes for the current slide in a side/bottom panel, with a toggle to hide them (e.g. when projecting this same window).
- A next-slide thumbnail and a slide counter (
3 / 12). - Navigation: on-screen prev/next, arrow keys / space,
Escto exit. Presenter mode owns the keyboard while active — the editor's tool and layer shortcuts (V/P/B, arrows, Delete, …) are suppressed until it exits. - The deck theme is composed in (see below) — presenter mode shows the fully themed slide.
v1 is single-window: good for rehearsal, review, and casual presenting. The true projector experience (audience window + separate presenter window with notes) is Phase E.
4. Deck in the community browser
A published deck is a normal kind-30078 event, so it appears in the feed like any design — which means it needs preview support:
- Feed-card thumbnail — the first slide's mini-render, with a
slide-count badge (e.g.
12 slides) so a deck reads as a deck at a glance. - Publish-confirm preview — likewise the first slide.
- Full preview page (
openDesignPreview) — carries the Start Presentation button (surface #3) alongside Use/Fork · Share · Save JSON.
These hook into the existing per-mode preview renderers and feed-card
builder; deck joins MODE_LABELS and the community mode filters.
Theme composition
The deck theme is applied at render time, non-destructively — the slide JSON is never mutated.
-
deck.themecarries a small fixed set for v1:fontFamily,background. -
When a slide is rendered in a deck context (deck thumbnails, presenter mode), each themed property resolves as:
effective = slide.ignoreDeckTheme ? slideValue : (deck.theme.<prop> || slideValue)e.g. a text layer's font = the deck theme font unless the slide opts out or the theme leaves it blank.
-
The slide editor shows the raw slide (no theme) — the theme only exists in the deck. This keeps the editor simple and the slide reusable across decks with different themes. When the slide is opened from a deck, the editor shows a notice that the theme will restyle it elsewhere (surface #1) so the editor/presenter mismatch isn't surprising.
-
ignoreDeckThemeis a per-slide boolean for v1 (whole-slide opt-out). Per-property locking is a future refinement.
Because it is pure JSON composition, the same logic serves deck thumbnails, presenter mode, and any future export.
Phased delivery
Phase A — Slide format
templateMode: 'slide'; 16:9 canvas;slideobject withnotes.- Slide Designer option in the Designer-tab Layout panel.
- Canvas panel with resolution presets + the Speaker notes textarea.
- Save / load / publish / fork a standalone slide design (
casewrap-slide).
Ship gate: create a 16:9 slide with notes, publish it, load it back — notes round-trip; it shows in the community browser.
Note: a standalone slide is thin value on its own — a 16:9 graphic whose notes don't pay off until Presenter Mode (Phase D). Phase A is groundwork; A + B together are the minimum genuinely-useful unit.
Phase B — Deck Builder
templateMode: 'deck'; thedeckdata model (embedded ordered slides).- Deck Builder option in the Template-tab Layout panel.
- The slide-sorter view: add (blank / import, with cross-aspect letterboxing) · reorder · duplicate · delete; open a slide in the canvas editor and write back on switch; per-slide thumbnails.
- Sorter-view canvas chrome (hidden zoom bar / options panel).
- Save / load / publish / fork a deck (
casewrap-deck), self-contained. - Deck feed-card thumbnail (first slide + slide-count badge) and
publish-confirm preview;
deckadded toMODE_LABELS/ mode filters.
Ship gate: build a 5-slide deck, reorder it, edit slide 3, save the deck, reload it — order and edits persist; the deck shows in the feed with its slide count.
Phase C — Deck theme
deck.theme(font family, background) + theme controls in the sidebar.- Per-slide
ignoreDeckThemetoggle. - Render-time theme composition for deck thumbnails.
Ship gate: set a deck font; all slides restyle in the thumbnails; toggle one slide's opt-out and it reverts to its own font.
Phase D — Presenter Mode (single-window)
- Full-screen runtime: current slide + notes + next thumbnail + counter.
- Keyboard / on-screen navigation; notes toggle;
Escto exit. - Theme composed in.
- Two
▶Present entry points: a tool-palette button in the Deck Builder editor, and a "Start Presentation" button on a deck's template-browser preview page.
Ship gate: present a deck start to finish with the keyboard from both entry points; notes track the current slide.
Phase E — Dual-window presenter (future)
- Popped-out audience window (slide only) synced to a presenter window (current + next slide, notes, timer). Not built in this pass.
Risks & tradeoffs
| Risk | Mitigation |
|---|---|
| A deck embeds N slides → large JSON / large Nostr event; relays may cap event size | Images are URLs, not binary, so size is bounded to layer JSON. Warn when the deck payload exceeds ~100 KB or ~25 slides. Escape hatch if this proves limiting: a future mode where the deck references slide naddrs instead of embedding them (Phase E). |
| Slide-edit ⇄ deck round-trip can drop edits if write-back is missed | Reuse the proven persist…/load… pattern; persist on every view switch and before publish/save/reorder/present. |
| Undo crossing the sorter ⇄ slide-editor boundary (one shared history stack) | Sorter ops and slide edits are ordinary stack entries; undoing an entry made in the other view switches to that view first, so undo always lands somewhere coherent. |
| Template/Designer framing is conceptually muddy (a deck is a sequence, not a print artboard) | Accepted explicitly; documented. Deck = Template-tab by analogy to the disc sheet; revisit if the IA is ever reworked. |
| Theme composition must not corrupt the slide's own data | Theme is applied purely at render time; slide JSON is never mutated; opt-out is a slide flag. |
| Presenter fullscreen behaviour varies across browsers | Use the standard Fullscreen API with a graceful non-fullscreen fallback. |
| The slide sorter is a brand-new editor surface | Isolated as Phase B with its own ship gate; the slide editor itself is the existing canvas, unchanged. |
Old clients loading a deck/slide payload |
Additive schema; unknown templateMode falls back gracefully (the community browser already filters by mode). |
Open questions
- Slide default resolution — DECIDED: 1920×1080 default (matches the existing Custom Art 16:9 preset), 1280×720 offered as an alternate preset. Aspect locked 16:9.
- Presenter scope — DECIDED: start single-window (Phase D); dual-window
presenter view is Phase E.
2a. Presenter entry points — DECIDED: a
▶Present button in the Deck Builder's tool palette, and a matching "Start Presentation" button (same icon) on a deck's preview page in the template browser. - Slide editing — DECIDED: the existing canvas editor, unchanged — a slide is just a 16:9 canvas.
- Deck theme — DECIDED: a deck-level theme overrides per-slide styling
(fonts, background); each slide has an opt-out (
ignoreDeckTheme) so it can keep its own look. Render-time composition, JSON-driven. - Speaker notes — plain text for v1; rich text is future.
- Deck Builder canvas UI — sorter-grid view that opens slides in the canvas editor for v1; an always-on filmstrip is deferred polish.
- Schema bump — increment
schemaVersionwhen Phase A lands;slide,deck,notes,ignoreDeckTheme, slidenameare additive. - Importing a non-16:9 design as a slide — DECIDED: scale-to-fit, centered and letterboxed on the 16:9 slide; no cropping.
- Deck in the community feed — DECIDED: feed-card and publish-confirm previews show the first slide, with a slide-count badge on the card.
- Deck → PDF / video export — DECIDED: non-goal for v1; Presenter Mode is how you show a deck. A natural future phase.
- Deck size limit — soft warning past ~100 KB / ~25 slides; hard
architectural fallback (reference slides by
naddr) deferred to Phase E if it proves necessary.
Acceptance summary
Phase A
- Slide Designer is a Designer-tab layout; canvas is 16:9.
- Speaker-notes field saves into
slide.notesand round-trips. - A slide publishes/loads/forks as a
casewrap-slidedesign.
Phase B
- Deck Builder is a Template-tab layout with a working slide sorter.
- Add / reorder / duplicate / delete slides; import a published design (a non-16:9 import lands letterboxed on the slide).
- Editing a slide writes back into the deck; deck saves self-contained.
- A published deck shows in the community feed with a slide-count badge.
Phase C
- A deck theme restyles every slide in the deck.
- A slide's
ignoreDeckThemetoggle reverts it to its own styling.
Phase D
- Presenter mode plays a deck full-screen with notes + navigation.
- Notes follow the current slide; the theme is applied.
- Presenter mode owns the keyboard — editor shortcuts don't fire.
- A
▶Present button launches it from the Deck Builder tool palette. - A "Start Presentation" button launches it from a deck's template- browser preview page.