Files
Artstr-Studio/docs/SLIDE_DECK_FEATURE.md
thePR0M3TH3ANandClaude Opus 4.7 9bf99f1abb Add Slide Decks & Presenter Mode (v1: Phases A–D)
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]>
2026-05-21 22:10:06 -04:00

487 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
1. 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.
2. 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.
3. 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.
4. 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 via `isCanvasMode()`), Slide Designer
layout on the Designer tab, 16:9-locked Canvas panel + Speaker notes
textarea (`state.customArt.notes`), and `slide` save / load / publish /
fork as `casewrap-slide`. Slides appear in the community browser.
- **Phase B — Deck Builder: done.** `templateMode: 'deck'` with the
embedded-slide `deck` data 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 via `persistEditingDeckSlide()`; deck save / load /
publish / fork as `casewrap-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-slide `ignoreDeckTheme` opt-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 a `n / total` counter. 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's `state.layers` for 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'` *or*
`templateMode === 'customart'`. The **Template tab** otherwise.
- Each tab has a Layout panel: `layoutFieldset` (Template — Case cover /
Disc labels) and `designerLayoutFieldset` (Designer — Custom Art / Disc
Design). `applyLayout()` maps a layout choice to the right
`appMode`/`templateMode` and crosses app modes when needed.
- Custom Art canvas state: `state.customArt = { width, height, background,
preset }`, rendered on `#sheet`; payload via `customArtPayload()`.
- Designs publish as kind-30078 parameterized replaceable events tagged
`casewrap-<mode>`, with the `d`-tag mode-prefixed.
- Save discriminator (`templateMode` in 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'`)
```js
{
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'`)
```js
{
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 optional `name` and
`ignoreDeckTheme`. Embedded slides carry **no `meta`** of their own — the
deck's `meta` covers the whole deck.
- **Import** copies a published design's `{ slide?/customArt, layers }` into
a new `deck.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`-tag `slide:<title|id>`.
- Deck: kind-30078, tag `casewrap-deck`, `d`-tag `deck:<title|id>`.
- Both are forkable and editable-in-place via the existing NIP-33 flow.
- Schema version bumps; `slide` / `deck` / `notes` / `ignoreDeckTheme` are
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 — the `persistCurrentDiscDesign`/`loadCurrentDiscDesignLayers`
pattern. 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, `Esc` to 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.theme` carries 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.
- `ignoreDeckTheme` is 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; `slide` object with `notes`.
- 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'`; the `deck` data 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; `deck` added to `MODE_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 `ignoreDeckTheme` toggle.
- 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; `Esc` to 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 `naddr`s 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
1. **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.
2. **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.
3. **Slide editing** — DECIDED: the existing canvas editor, unchanged — a
slide is just a 16:9 canvas.
4. **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.
5. **Speaker notes** — plain text for v1; rich text is future.
6. **Deck Builder canvas UI** — sorter-grid view that opens slides in the
canvas editor for v1; an always-on filmstrip is deferred polish.
7. **Schema bump** — increment `schemaVersion` when Phase A lands; `slide`,
`deck`, `notes`, `ignoreDeckTheme`, slide `name` are additive.
8. **Importing a non-16:9 design as a slide** — DECIDED: scale-to-fit,
centered and letterboxed on the 16:9 slide; no cropping.
9. **Deck in the community feed** — DECIDED: feed-card and publish-confirm
previews show the first slide, with a slide-count badge on the card.
10. **Deck → PDF / video export** — DECIDED: non-goal for v1; Presenter
Mode is how you show a deck. A natural future phase.
11. **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.notes` and round-trips.
- [ ] A slide publishes/loads/forks as a `casewrap-slide` design.
### 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 `ignoreDeckTheme` toggle 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.