docs(book): rewrite spec around Nostr-native sidecar architecture

Apply twelve Nostr-driven revisions to the Book Designer spec:

1. **Sidecar architecture.** Pages, masters, and the cover spread
   are now separate addressable kind-30078 events
   (`casewrap-page`, role-discriminated). The book is a thin
   `casewrap-book` manifest that references them by
   `{ kind, pubkey, dTag }` — the same shape Linked Designs
   already speaks. Edit one page → that page event replaces; the
   manifest only changes when order / list / spec moves.
2. **Masters as Nostr objects.** Each master is its own event; one
   "Body pages" master can be referenced by many books and forked
   by other authors. Default master lives on the manifest spec.
3. **Cover as a sidecar event** with `role: 'cover-spread'` so it
   can iterate independently and be republished as standalone
   marketing art.
4. **Linked Designs is the precedent.** The resolver IIFE,
   progressive fetch, localStorage disk cache, batched relay
   queries, upstream-update notifications, and attribution chain
   all transfer verbatim. Call this out explicitly so we don't
   reinvent the wheel.
5. **Addressable refs everywhere.** Manifest entries use the
   existing `{ kind, pubkey, dTag }` form (convertible to naddr at
   the UI layer), not local UUIDs.
6. **Premium gating + tips.** New "Premium, tipping, gating"
   section covers whole-book paywall (gate the manifest),
   per-chapter paywall (gate individual page events via
   PREMIUM_DESIGNS), and NIP-57 zaps at any granularity.
7. **Reader Mode** added as Phase G — the Slide Decks' Presenter
   Mode counterpart, with progressive page fetch, TOC sidebar,
   page-flip nav, and inline zap-to-unlock.
8. **Embed integration.** EXTERNAL_EMBED learns `?book=<naddr>`
   for a flip-book iframe player; same transport + CSP rules.
9. **Per-page reactions / comments** work for free because each
   page is its own addressable event (NIP-25 + kind-1). One-liner
   in product goals + Reader Mode acceptance.
10. **`{page-count}` semantics pinned.** Defaults to *numbered*
    pages; new `{physical-page-count}` exposes the raw total.
    Plus `{book-author}` for the manifest pubkey.
11. **NIP-23 distinction.** Explicit non-goal — Book mode is
    fixed-layout design, not reflowable prose; for prose use
    NIP-23 long-form articles directly.
12. **Manifest schema.** Pages now carry
    `{ ref, label?, inheritsMasterRef, skipNumber, gating? }`;
    page-side fields that vary per-book (master, skipNumber,
    label) moved off the page event onto the manifest entry.

Phase D split into D1 (publish individual page / master / cover
events) and D2 (publish the manifest); new Phase G (Reader Mode +
embed); new Phase H (premium gating). Acceptance summary,
cross-surface reactivity, and risks updated to match.

TODO.md's Book Designer summary rewritten to reflect the
sidecar plan.
This commit is contained in:
thePR0M3TH3AN committed 2026-06-02 10:36:40 -04:00
1 parent 50eed8c017
commit 33ee2b7ec5
2 files changed
+529 -217

No files matched your search

+26 -7
View File
@@ -8,18 +8,37 @@ Running list of deferred work. Detailed specs for the larger items live in
_(Empty — see "Shipped (recent)" below for the items that just
landed. Drop new items in here as they come up.)_
## Book Designer (unified flow)
## Book Designer (unified flow, Nostr-native)
Drafted spec at `docs/BOOK_DESIGNER_FEATURE.md`. New top-level
`templateMode: 'book'` that unifies the cover designer (front +
spine + back), the deck overview (pages grid), and the custom-art
canvas (per-page editor) around a shared `state.book.spec` (trim
size, page count, paper thickness, bleed). Phased delivery:
A (spec + skeleton) → B (pages + per-page editor) → C (master
pages + `{page-number}` tokens) → D (Nostr publish as
`casewrap-book`) → E (multi-page PDF export via vendored
`pdf-lib`) → F (templates + deck-to-book import). Big arc; v1 is
fixed-layout only — no reflowable text or paragraph-style engine.
size, page count, paper thickness, bleed).
Pages, masters, and the cover spread are **separate addressable
Nostr events** (`casewrap-page` kind-30078, `role`-discriminated)
referenced by a thin **`casewrap-book` manifest** — same sidecar
pattern Linked Designs already uses. Edit one page → that page
event replaces; the manifest is unchanged unless the page list
or order moves. Per-page reactions, comments, tips, and premium
gating come for free because each page is its own event.
Phased delivery:
- **A** spec + book mode skeleton
- **B** pages overview + per-page editor + token substitution
- **C** master pages with single-level inheritance
- **D1** publish individual page / cover / master events
- **D2** publish the `casewrap-book` manifest
- **E** multi-page PDF export via vendored `pdf-lib`
- **F** templates + deck-to-book import
- **G** Reader Mode (Slide Decks' Presenter Mode counterpart) +
EXTERNAL_EMBED `?book=<naddr>` flip-book
- **H** per-page + whole-book premium gating using PREMIUM_DESIGNS
Big arc; v1 is fixed-layout only — no reflowable text or
paragraph-style engine. Reflowable prose belongs on NIP-23
long-form articles, not here.
## Design-tool follow-ups (Illustrator parity arc)
+503 -210
View File
@@ -3,7 +3,7 @@
## Goals
A unified document type for designing **books** — a cross between InDesign
and Illustrator inside Artstr Studio. One document combines a cover spread
and Illustrator inside Artstr Studio. One book combines a cover spread
(front + spine + back), an ordered list of interior pages, and per-page
design surfaces, all driven by a shared **document spec** (trim size,
bleed, page count, paper thickness) so that changes propagate
@@ -16,41 +16,62 @@ The three existing workflows are the building blocks:
table of contents.
- **Custom-art canvas** → the per-page design surface.
A **Book** document type orchestrates all three around the shared spec and
exports the whole thing as a multi-page PDF.
A **Book** document type orchestrates all three around the shared spec,
exports the whole thing as a multi-page PDF, and publishes to Nostr as a
**manifest event** that references each page / cover / master as its
own addressable kind-30078 event — the same sidecar pattern Linked
Designs already uses.
### Product goals
1. **A single Book document** that holds a cover spread, an ordered list
of interior pages, and a document spec. The document is one
self-contained Nostr-publishable artefact (`casewrap-book`).
2. **Auto-computed spine width** from `pageCount × paperThicknessIn`. The
cover designer's spine column is derived, not user-typed; the cover
re-renders whenever pages are added or removed.
3. **Uniform page artboards**. Every interior page inherits trim width /
height / bleed from the spec, so resizing the trim updates every page
in one shot.
4. **Pages overview** that reuses the deck-sorter UI: drag-reorder,
add-blank, duplicate, delete, label, jump-to-edit. Page numbers render
on each thumbnail.
5. **Per-page editor** that reuses the Custom Art canvas — same tools,
1. **A Nostr-native book**, not a monolithic blob. The manifest event
carries the spec + an ordered list of `naddr` refs. Each page, each
master, and the cover are separate addressable events. Edits to one
page replace only that page's event; the manifest changes only on
structural edits (order, list, spec).
2. **A single Book document UX**. End-users see one cohesive editor —
the sidecar plumbing is invisible. Opening a book event fans out to
resolve every referenced page (with progressive fetch and a
localStorage disk cache, just like Linked Designs).
3. **Auto-computed spine width** from `pageCount × paperThicknessIn`.
The cover designer's spine column is derived, not user-typed; the
cover re-renders whenever pages are added or removed.
4. **Uniform page artboards**. Every interior page inherits trim
width / height / bleed from the spec, so resizing the trim updates
every page in one shot.
5. **Pages overview** that reuses the deck-sorter UI: drag-reorder,
add-blank, duplicate, delete, label, jump-to-edit. Page numbers
render on each thumbnail.
6. **Per-page editor** that reuses the Custom Art canvas — same tools,
same layer model, same renderer — but knows it's inside a book so
prev / next page navigation, page-number context, and a small
"page X / N" header are present.
6. **Master pages** (Indesign-style inheritance): a master is a special
page whose layers render *underneath* every page that inherits from
it. Used for running headers / footers, page numbers, baseline grids,
margin guides.
7. **Multi-page PDF export** that stamps each page in order then the
"page X / N" header are present. Saving a page autosaves the
per-page event; saving the book updates the manifest.
7. **Master pages** (Indesign-style inheritance) **as reusable Nostr
objects**. A master is its own addressable event; one master can be
referenced by many books (and forked by other authors). Masters
render *underneath* every page that inherits from them — running
headers / footers, page numbers, baseline grids, margin guides.
8. **Multi-page PDF export** that stamps each page in order then the
cover spread on its own page, with optional bleed marks and crop
marks for print.
9. **Per-page premium / tips** (free Phase D follow-on). A book's
manifest can gate individual pages via the existing PREMIUM_DESIGNS
pipeline, enabling "first chapter free, rest paid" flows. NIP-57
zaps on the manifest tip the whole book; zaps on a page tip a
chapter.
10. **Per-page reactions and comments** — because each page is its own
addressable event, NIP-25 reactions and kind-1 comments work per
page for free, no extra plumbing.
### Non-goals (v1)
- **Reflowable text / paragraph styles / threaded text frames.** A book
in Artstr is fixed-layout; you design each page as a canvas, not as
a continuous text flow with master typography. (A natural follow-up
arc, but huge.)
a continuous text flow with master typography. For reflowable prose,
Nostr already has **NIP-23 long-form articles** (kind-30023, markdown
body) — Book mode is explicitly the *design* counterpart, not a
competing prose format.
- **Footnote / endnote engine, index, automatic TOC, cross-references.**
These all depend on a text-flow model we don't have. Manual TOC
pages are fine.
@@ -62,11 +83,10 @@ exports the whole thing as a multi-page PDF.
- **Page-size variance within a single book.** Every interior page has
the same trim / bleed (only the cover differs). Mixed-size books are
out of scope.
- **Linked design pulls inside pages.** The existing Linked Designs
feature still works inside book pages, but pages aren't published
individually — only the whole book.
- **Spreads-as-design-units** (facing-page editing). v1 is single-page
layout per page. Facing-page guide overlays can come later.
- **Chained master inheritance.** A master can be inherited by a page
but not by another master. Single level only in v1.
### Implementation status
@@ -76,36 +96,54 @@ Not started. This document is the v1 spec.
## Why this fits the repo
Most of the primitives are already in place; Book mode is **glue + spec**,
not a new engine.
Most of the primitives are already in place; Book mode is **glue + spec +
manifest**, not a new engine.
- **Linked Designs gives us the sidecar architecture for free.** The
resolver IIFE, schema discriminator, progressive fetch, localStorage
disk cache, batched relay queries, upstream-update notifications,
attribution chain (p-tags + link-tags) — every piece transfers
verbatim. A book manifest is functionally a *deck of linked designs*
with cover + masters metadata layered on. Phases 1a–3d of Linked
Designs are direct dependencies.
- **Layer model + targets.** Each layer already keys to a `target`
(`page` / `cover-trim` / `back` / `spine` / `designer-disc` / `canvas`).
Adding `book-page:<id>` / `book-cover-front` / `book-cover-back` /
`book-spine` / `book-master:<id>` targets is additive.
Adding `book-page` / `book-cover-front` / `book-cover-back` /
`book-spine` / `book-master` targets is additive.
- **Per-target rendering.** `renderLayers` already filters by target and
delegates to the right host; the cover designer, deck, and custom-art
all coexist in one document with no special-casing in the renderer.
- **Deck overview grid.** The deck sorter already does thumbnail-grid
reordering with drag-and-drop. Reusing it for pages is a relabel +
one data-shape rename away.
ref-shape rename away.
- **Custom-art canvas as page editor.** The page editor *is* the Custom
Art canvas. The book wraps it in prev/next nav and a page-context bar.
- **Cover designer.** The cover view *is* the existing cover designer,
except the spine width is computed from the spec instead of typed.
- **PREMIUM_DESIGNS** already implements per-event gating (watermarked
preview + zap-unlocked content). A book gates individual page events
with the same machinery — no new crypto path.
- **Nostr publish + fork + embed.** Books reuse the existing publish
pipeline as kind-30078 with a new `casewrap-book` tag.
pipeline; each constituent event reuses the same kind-30078 with new
tag names.
What's actually new:
- **`state.book`** document spec + page list shape.
- **`state.book`** as a manifest plus a hydrated cache of referenced
page / master / cover objects.
- **Manifest publish flow** that fans out per-page autosave events and
separately versions the manifest.
- **Spine derivation** when page count or paper thickness changes.
- **Master pages** (a new render pass: master layers render below page
layers, scoped by `inheritsMasterId`).
- **Master pages** as a new event type with a single-level inheritance
render pass.
- **Page-aware navigation** (prev / next, page numbers).
- **Multi-page PDF export.** Currently there is no PDF export at all;
Custom-art exports PNG/JPG and the cover designer exports per-panel
PNG. Books need one PDF stamped with every page in order.
- **Reader Mode** (analogous to the Slide Decks' Presenter Mode) — a
full-screen page-flip viewer driven off the manifest.
- **Book embed** — a flip-book player in the EXTERNAL_EMBED iframe
transport.
- **Document settings modal** that owns the spec and propagates changes.
---
@@ -114,112 +152,202 @@ What's actually new:
The existing top-level modes:
| Mode | Selector | Surfaces |
|------------------|-------------------------|-----------------------------------------------------|
| Cover | `templateMode: 'cover'` | `cover-trim` / `front` / `back` / `spine` |
| Disc | `templateMode: 'disc'` | `designer-disc` |
| Custom Art | `templateMode: 'customart'` | `canvas` |
| Slide | `templateMode: 'slide'` | `canvas` (16:9 alias) |
| Deck | `templateMode: 'deck'` | embedded slides via `deck.slides[]` |
| Pixel-art | `templateMode: 'pixelart'`| `canvas` (palette-indexed) |
| **Book** (new) | `templateMode: 'book'` | `book-cover-front` / `book-cover-back` / `book-spine` / `book-page:<id>` / `book-master:<id>` |
| Mode | Selector | Surfaces |
|------------------|---------------------------|-----------------------------------------------------|
| Cover | `templateMode: 'cover'` | `cover-trim` / `front` / `back` / `spine` |
| Disc | `templateMode: 'disc'` | `designer-disc` |
| Custom Art | `templateMode: 'customart'` | `canvas` |
| Slide | `templateMode: 'slide'` | `canvas` (16:9 alias) |
| Deck | `templateMode: 'deck'` | embedded slides via `deck.slides[]` |
| Pixel-art | `templateMode: 'pixelart'`| `canvas` (palette-indexed) |
| **Book** (new) | `templateMode: 'book'` | `book-cover-front` / `book-cover-back` / `book-spine` / `book-page` / `book-master` |
Book mode adds a third sidebar tab ("Pages") alongside Designer (per-page
canvas) and Template (cover view). All three views read and write the same
`state.book` document.
`state.book` document, which hydrates page / master / cover refs from
Nostr via the Linked Designs resolver.
---
## Data model
### Book (`templateMode: 'book'`)
The book is a **manifest event** that references its constituent parts.
Page, master, and cover events live as their own kind-30078 events and
each carries a single design surface plus the metadata needed to render
it.
### Addressable refs
Every reference inside the manifest uses the existing addressable-event
shape Linked Designs already speaks:
```js
{ kind: 30078, pubkey: '<hex>', dTag: '<d-tag>' }
```
Convertible to / from `naddr` at the UI layer.
### Book manifest (`templateMode: 'book'`, tag `casewrap-book`)
```js
{
version: SCHEMA_VERSION,
templateMode: 'book',
templateType: 'book',
meta: { … }, // title / category / language / tags
meta: { … }, // title / category / language / tags
book: {
spec: {
trimW: 6.0, // inches; common preset: US Trade
trimW: 6.0, // inches; common preset: US Trade
trimH: 9.0,
bleed: 0.125, // outside each trim edge
paperThickness: 0.0042, // inches per page; default = uncoated 60# text
spineFromPages: true, // false → spineW is user-typed instead
spineWManual: 0.5, // used only when spineFromPages = false
coverHasSpine: true, // softcover yes, single-spread chapbook no
coverHasBack: true, // wraparound vs front-only
bleed: 0.125, // outside each trim edge
paperThickness: 0.0042, // inches per page; default = uncoated 60# text
spineFromPages: true, // false → spineWManual is used
spineWManual: 0.5,
coverHasSpine: true, // softcover yes, single-spread chapbook no
coverHasBack: true, // wraparound vs front-only
pageNumbering: {
enabled: true,
startAt: 1,
firstNumberedPage: 1, // skip cover, title, etc.
format: '{n}', // '{n}' / 'Page {n}' / '— {n} —'
firstNumberedPage: 1, // index into `pages` — skip cover, title, etc.
format: '{n}', // '{n}' / 'Page {n}' / '— {n} —'
},
bookSize: 'US Trade 6×9', // label only; matches a preset
bookSize: 'US Trade 6×9', // label matching a preset
defaultMasterRef: { … } | null, // applied to new pages on creation
},
cover: {
front: { background: '#ffffff', layers: [ … ] },
back: { background: '#ffffff', layers: [ … ] }, // omitted if !coverHasBack
spine: { background: '#ffffff', layers: [ … ] }, // omitted if !coverHasSpine
},
masters: [
{
id: 'master:body', // referenced by page.inheritsMasterId
name: 'Body pages',
background: '#ffffff',
layers: [ … ], // page-number text, header rule, etc.
renderBelow: true, // master layers below page layers (always true v1)
},
…
],
cover: { ref: { kind, pubkey, dTag } }, // a `casewrap-page` with role:'cover-spread'
pages: [
{
id: 'page:abc123',
name: 'Cover page', // optional label for the sorter
inheritsMasterId: 'master:body' | '', // '' = no master
background: '', // '' = inherit master / spec; '#hex' overrides
skipNumber: false, // true = numbered as blank (e.g. title page)
layers: [ … ],
ref: { kind, pubkey, dTag }, // a `casewrap-page` with role:'interior'
label: 'Intro', // optional sorter label override
inheritsMasterRef: { kind, pubkey, dTag } | null, // overrides defaultMasterRef
skipNumber: false, // true = excluded from {page-number} sequence
gating: null | { // optional, opt-in per page
mode: 'free' | 'premium',
previewWatermark: true, // PREMIUM_DESIGNS pipeline
// unlock details live on the gated page event itself
},
},
… // ordered list, variable length
… // ordered list, variable length
],
masters: [ // refs only; the book can use a master without listing here, but listing surfaces them in the Masters modal
{ ref: { kind, pubkey, dTag }, label: 'Body pages' },
…
],
},
}
```
#### Shape notes
### Page event (kind-30078, tag `casewrap-page`)
- A book is **self-contained**: every page and master embeds its full
layer array. Same model as `deck.slides[]`. The whole book publishes as
one Nostr event.
- Each **embedded surface** (cover panel, master, page) carries only its
per-surface fields — no `meta`, no `templateMode`. Document-level
metadata covers the book.
- Layer `target` for a page layer is `book-page:<id>`. For a master:
`book-master:<id>`. Cover panels: `book-cover-front` / `…-back` /
`…-spine`. The renderer filters by target as it already does — no
cross-talk between pages, no leak from one master into another.
- The `pages` array order **is** the print order. The sorter mutates
this array directly.
```js
{
version: SCHEMA_VERSION,
templateMode: 'book-page',
templateType: 'book-page',
meta: { … }, // page title / language / optional thumbnail hash
page: {
role: 'interior' | 'cover-spread' | 'master',
// Sizing: a page snapshots the spec values that produced it so it
// renders correctly even if opened standalone (e.g. via embed).
trimW: 6.0,
trimH: 9.0,
bleed: 0.125,
coverSpread: null | { // present only when role: 'cover-spread'
spineW: 0.5,
hasSpine: true,
hasBack: true,
},
background: '', // '' = inherit master / spec; '#hex' overrides
layers: [ … ], // standard layer array; targets are book-page, book-master, or book-cover-*
},
}
```
#### Derived values
#### Notes on the page event
- `spec.spineW` = `spineFromPages ? pageCount × spec.paperThickness : spec.spineWManual`.
- `pageCount` = `book.pages.length`.
- Cover-spread total W = `(2 × trimW) + spineW + (2 × bleed)`.
- A page's drawable area = `trimW × trimH` (bleed renders outside but is
trimmed in the printed product; gutter / margins are master-page
responsibilities, not spec-driven).
- **One surface per event.** Whether interior, cover, or master, the
event carries a single design surface so it can be opened
standalone (Linked Designs viewer, embed iframe, etc.) without
needing the parent book to interpret it.
- **No order or numbering inside the page itself.** Position in the
book and `skipNumber` live on the manifest; the page is reusable.
- **`{page-number}` tokens** in text layers resolve at render time
using context from whichever manifest is rendering the page (or
show a placeholder when previewed standalone).
- **Independent publish life-cycle.** Page events have their own
`d`-tag (e.g. `book-page:<title|id>` or a stable opaque id) so
NIP-33 replaceability works per page. Edit one page → that page
event replaces; the book manifest is unaffected unless the
user changes the page list or order.
### Master event
Same shape as a page event with `role: 'master'`. Masters do not
typically declare `trimW` / `trimH` themselves (they inherit from the
book), but they snapshot the spec at publish time so a stand-alone
preview is sane.
### Cover event
Same shape as a page event with `role: 'cover-spread'`. The
`coverSpread` block records the spine width and whether the spread
includes spine / back. Layers carry `book-cover-front` /
`book-cover-back` / `book-spine` targets so the renderer can lay them
out side-by-side.
### Why a separate event for each surface
| Concern | Inline-blob book | Sidecar book |
|----------------------------------|------------------|--------------|
| 200-page book event size | Tens of MB; relays reject | Manifest stays small (~10 KB); pages fetched on demand |
| Edit one page | Re-publishes whole book | Replaces only that page's event |
| Reuse a master across books | Copy-paste each time | One master event, many manifests reference it |
| Forks of one chapter | Must clone whole book | Fork just the page event |
| Per-page comments / reactions | All collapse to the book | Each page receives its own |
| Per-page premium / tips | Whole book or nothing | First-chapter-free is natural |
| Reader fetches as you scroll | Must download everything up front | Progressive fetch via the resolver |
### Nostr
- Book: kind-30078, tag `casewrap-book`, `d`-tag `book:<title|id>`.
- Single event carries cover + every page + every master inline. A large
book may exceed the relay's max event size; if so, splitting masters /
pages into per-event sidecars (à la Linked Designs) is a v2 concern.
- Schema-version bump; new `book` block is additive — old clients render
nothing for a book event but don't break.
- **Book manifest**: kind-30078, tag `casewrap-book`, `d`-tag
`book:<title|id>`.
- **Page / master / cover**: kind-30078, tag `casewrap-page`, `d`-tag
`book-page:<title|id>` (or `book-master:`/`book-cover:` if we want
three tags for easier filter UX). Decision: **one tag
(`casewrap-page`) with `role` differentiating** — fewer filters for
clients, the `role` field still discriminates.
- All four event types are forkable and editable-in-place via the
existing NIP-33 flow.
- Schema-version bump; the new `book` block + new event types are
additive. Old clients see an unknown templateMode and either skip or
render a stub.
- **Attribution.** Manifest p-tags every author that contributed a
referenced page / master / cover (already how Linked Designs Phase
3a handles attribution).
- **NIP-89 / kind-31990** client recommendations: a future arc; for v1
Artstr is the only client that opens these by URL.
---
## Token substitution + page-count semantics
A text layer's HTML can contain magic tokens that resolve at render
time using the manifest's context.
- `{page-number}` → the current page's number under the manifest's
`pageNumbering` config. A skipped page renders no number (its
`{page-number}` token resolves to empty).
- `{page-count}` → total **numbered** pages (counts excluding any
`skipNumber: true` entries). This is what users mean when they say
"page 3 of 240."
- `{physical-page-count}` → total pages in the manifest, including
skipped ones. Rarely useful but exposed for completeness.
- `{book-title}` → manifest `meta.title`.
- `{book-author}` → manifest `pubkey` resolved to a profile name.
Tokens resolve in both the editor preview and rendered output. Standalone
preview (a page event opened without its manifest) shows tokens with a
greyed-out placeholder (`[page]`, `[N]`).
---
@@ -242,39 +370,50 @@ book view. Fields:
- **Cover layout**: wraparound (front + spine + back) / front-only.
- **Page numbering**: enabled toggle + start-at, first-numbered-page,
format string.
- **Default master**: dropdown of refs registered in `book.masters`.
Editing **trim** prompts confirmation if any page already has layers —
"Layers may need reflowing." Apply re-runs page-bounds calc and refits
the cover surfaces. Bleed change does not warn (it only affects guides).
Editing **trim** prompts confirmation — "Page events that snapshotted
the old trim won't auto-resize; you may need to re-publish each page."
Apply re-runs cover surface fit and pushes the new spec into newly
created pages.
### 2. Cover view
Reuses the existing cover designer host. Differences:
Reuses the existing cover designer host, bound to whatever event the
manifest's `cover.ref` resolves to. Differences from the standalone
cover designer:
- The spine column width is **read-only** when `spineFromPages = true`;
shows the computed value with a hint ("derived from N pages × paper
thickness"). When manual, the existing typed-in field is editable.
- Front / back / spine targets renamed under the hood to
`book-cover-front` / `book-cover-back` / `book-spine` so layers don't
collide with a stand-alone cover document opened in the same session.
- A "Replace with template" picker scoped to the **`book-cover` template
category** (a new kind alongside `casewrap-template`).
thickness"). When manual, the existing typed-in field is editable
and writes back to the spec.
- Saving the cover publishes the cover event (via the page autosave
flow); the manifest is updated only if the cover ref needs to change
(e.g. forking a referenced cover into a local copy for edits).
- A "Replace cover with…" picker scoped to the **`casewrap-page` events
with `role: 'cover-spread'`** category.
### 3. Pages overview
Reuses the deck-sorter component. Differences:
- Title says "Pages" with a `Total: N` counter.
- Each tile shows the page number (from `pageNumbering`) below the
thumbnail. Skipped or unnumbered pages show no number.
- Drag-reorder writes back to `book.pages`. Numbering re-renders live.
- Per-tile menu: **Edit** (jumps to the per-page editor), **Duplicate**,
**Delete**, **Apply master…** (pick from `book.masters`), **Skip
numbering**, **Rename**.
- A toolbar above the grid: **+ Add page**, **+ Add 10 pages** (common
bulk action), **Import page from Nostr** (paste a slide / custom-art
event id and convert), **Manage masters…** (modal listing
`book.masters`).
- Title says "Pages" with a `Total: N` counter (numbered / physical
shown separately).
- Each tile shows the page number below the thumbnail, derived from
the manifest's `pageNumbering`. Skipped pages show no number.
- Drag-reorder writes back to `book.pages[]`. Numbering re-renders
live; the manifest event is marked dirty for republish.
- Per-tile menu: **Edit** (jumps to the per-page editor), **Duplicate**
(creates a new page event seeded with the current one's layers,
appends to manifest), **Delete** (removes from manifest; the page
event itself is left untouched on relays — manifest reference just
drops), **Apply master…** (pick from `book.masters`), **Skip
numbering**, **Rename**, **Open standalone** (opens the page event
in a fresh tab as if you imported it via Linked Designs).
- A toolbar above the grid: **+ Add page**, **+ Add 10 pages**,
**Import page from Nostr** (paste a `casewrap-page` event id;
manifest gets a ref to it without re-publishing the page),
**Manage masters…**, **Document settings**.
### 4. Per-page editor
@@ -295,36 +434,105 @@ The existing Custom Art canvas with three wraps:
conceptually). Trim is dashed red, bleed is dashed teal, optional
margin / gutter from the spec is faint grey.
The tool palette, layer panel, alignment / pathfinder / pen tools —
all unchanged. The canvas binds to `book-page:<id>` (or
`book-master:<id>` in master-edit mode), so new layers land on the
right page.
Editing a layer marks the **page event** as dirty for autosave. Saving
publishes that page event (NIP-33 replace). The manifest is only marked
dirty if the user changes the page's `inheritsMasterRef`, `skipNumber`,
or `label` (manifest-owned fields).
### 5. Masters modal
Lists `book.masters` with per-row Edit / Duplicate / Delete /
Rename / Set-as-default. "+ New master" creates a blank entry.
"Edit" opens the per-page editor in master-edit mode.
Lists `book.masters` refs. Per-row Edit / Duplicate / Delete from
list / Rename / Set-as-default. "+ New master" creates a new
`casewrap-page` event with `role: 'master'` under the user's pubkey
and appends its ref to the manifest. "Import master from Nostr" pastes
in an existing master ref so the book can reuse another author's
master.
### 6. Reader Mode
Analogous to the Slide Decks' Presenter Mode. A full-screen page-flip
viewer driven off the manifest. Pulls page events progressively as the
user moves forward (with prefetch of next 2 pages); previously-viewed
pages cached in localStorage via the existing Linked Designs disk
cache.
- Keyboard nav: → / Space / PageDown advance; ← / PageUp back; Home /
End jump to first / last; Esc exits.
- Mouse / touch: click right half advances, left half retreats; swipe
on touch.
- Sidebar TOC: scrollable list of page labels with current-page
highlight; click to jump.
- Page count footer: `12 / 248` plus a thin progress bar.
- Premium-gated pages render the watermarked preview by default; an
inline "Unlock with zap" CTA opens the existing PREMIUM_DESIGNS
modal.
Launches from a ▶ Read button on the manifest's preview pane and a
keyboard shortcut from the Pages overview.
### 7. Embed integration
The EXTERNAL_EMBED viewer learns a `?book=<naddr>` mode that renders
the manifest in a flip-book player (cut-down Reader Mode without the
TOC sidebar; arrows + swipe only). Re-uses the same iframe transport
and CSP rules; no new origin work needed.
---
## Cross-surface reactivity rules
1. **Page count changes** (add / delete / paste) → `spec.spineW`
1. **Page count changes** (add / delete / reorder) → `spec.spineW`
recomputed if `spineFromPages` → cover view re-renders with the new
spine column width → cover layers re-clamped if any straddled the
old spine boundary (probably warn but don't auto-move).
2. **Trim size changes** → every `book.pages[].layers[]` and the cover
panel sizes recompute. Layers are NOT auto-scaled — they keep their
absolute inch coordinates. Users get a one-time toast prompting
"Layers may now sit outside the trim — review pages."
3. **Bleed changes** → guide overlays update; layer coordinates
untouched.
4. **Master edit** → every page that inherits that master re-renders.
5. **Page-numbering format change** → all numbered page thumbnails
re-render their badges. The numbering doesn't get *baked* into page
layers; it lives on the master via a special `{page-number}` token
in a text layer.
old spine boundary (warn but don't auto-move) → manifest marked
dirty.
2. **Trim size changes** → cover surface sizes recompute. Existing
page events keep their snapshotted trim (since their content was
designed for that size); a "Re-publish snapshot" action regenerates
the page event's `trimW` / `trimH`. Users get a one-time toast.
3. **Bleed changes** → guide overlays update; layer coordinates and
page events untouched.
4. **Master edit** → master event republishes. Every page referencing
that master re-renders on next view (the Linked Designs auto-update
notification surfaces the new version).
5. **Upstream page / master / cover updated externally** → Linked
Designs already does this: a "New version available" badge in the
sorter tile / cover-view header; user clicks to accept and refetch.
6. **Page-numbering format change** → all numbered page thumbnails
re-render their badges. Numbering is never *baked* into page layers;
it lives only as `{page-number}` token text on a master (or wherever
the author chose to place it). Manifest marked dirty.
7. **Duplicate page** → creates a new page event under the user's
pubkey (not a manifest-internal copy); both the original and the
duplicate are independent events going forward.
---
## Premium, tipping, gating
PREMIUM_DESIGNS already implements per-event premium gating. Books
layer on top with **per-page gating** as a v1 capability and
manifest-level gating for whole-book paywalls.
- **Free book.** Default. Manifest and every page event publish without
premium fields.
- **Whole-book paywall.** Gate the **manifest event** itself with
PREMIUM_DESIGNS' encrypted-publish path. Readers see metadata + a
cover thumbnail; unlocking the manifest gives them refs to (already
free) page events. Cleanest model when the author wants to gate
everything.
- **First-chapter-free / rest paid.** Gate **individual page events**.
The manifest stays free (so previews / discovery work). When the
reader hits a gated page, Reader Mode shows the watermarked preview
and an unlock CTA. Each page can have its own price.
- **Tips.** NIP-57 zaps work on whatever event the user is zapping —
manifest, individual page, or master. The author's lightning address
on their profile is the destination as usual.
The manifest's per-page `gating` object is a hint to clients; it does
not duplicate the crypto state, which lives on the page event itself.
Clients should always verify gating by inspecting the page event when
loading it.
---
@@ -352,19 +560,21 @@ text, fonts, page management. Vendored as
### Pipeline
For each page (and each cover panel):
For each page (resolved via the manifest):
1. Build a **virtual canvas** at the page's pixel resolution
(e.g. `trimW × 300dpi`).
2. Run the existing `renderLayers` pipeline against that virtual
canvas. This already handles every layer type (shape, image, text,
QR, pixelart, linked design, deck slide, pathfinder result).
3. Take the rendered output and stamp it into a new PDF page via
3. Token-substitute `{page-number}` etc. at render time using the
manifest's `pageNumbering` config.
4. Take the rendered output and stamp it into a new PDF page via
`pdf-lib`. Text layers ideally get re-emitted as PDF text objects
(vector + searchable / selectable); v1 may flatten text to vector
paths to dodge font-subset complexity, with a follow-up to switch
to true PDF text.
4. Cover page is single-PDF-page with the full spread; print shops
5. Cover page is single-PDF-page with the full spread; print shops
typically want it as one wide spread, so a separate "Cover-only
PDF" download is also offered.
@@ -387,75 +597,92 @@ Cover view toolbar opens an export modal:
for SVG output. We accept it and flag as a phase B PDF improvement.
- No bookmarks / outline / TOC links inside the PDF.
- No press-quality CMYK profile.
- Premium-gated pages: by default the export refuses to include them
if the user is not the author or hasn't unlocked them. A user
exporting their own book gets everything; a reader who has zapped to
unlock can export the unlocked subset.
---
## Master pages
A master is a page-shaped surface whose layers paint *under* every
page that inherits it. Used for:
A master is a page-shaped Nostr event (kind-30078, `casewrap-page`,
`role: 'master'`) whose layers paint *under* every page that inherits
it. Used for:
- Running headers / footers
- Page numbers (via the `{page-number}` text token — the renderer
substitutes it per page at render time)
- Page numbers (via `{page-number}` tokens — substituted per page at
render time)
- Margin guides drawn as faint shape layers (optional)
- Background art / chapter dividers
### Inheritance rule
- A page declares `inheritsMasterId: 'master:body'`. Render: master
layers first (filtered by their `book-master:body` target), then page
layers on top. Master layers are non-interactive in the page editor.
- Multi-master: a master can itself inherit another master via
`inheritsMasterId` on the master record. (v1 supports a single level
of inheritance to keep the render simple — chained inheritance is
follow-up.)
- Default master: `book.spec.defaultMasterId` — applied to any page that
doesn't specify its own. New pages adopt it on creation.
### `{page-number}` token
A text layer whose HTML contains `{page-number}` gets per-page
substitution at render time. The same token works inside any text layer
but is primarily useful on master pages. Substitution:
- `{page-number}` → current page number (per `pageNumbering` format).
- `{page-count}` → total numbered pages.
- `{book-title}` → `meta.title`.
Tokens render at the live value in the page editor preview too.
- A page-list entry declares
`inheritsMasterRef: { kind, pubkey, dTag }`. Render: master layers
first (filtered by their `book-master` target), then page layers on
top. Master layers are non-interactive in the page editor.
- **Single level of inheritance only in v1** — masters cannot inherit
from other masters. Chained inheritance is a follow-up.
- Default master: `spec.defaultMasterRef` — applied to any page
whose `inheritsMasterRef` is null. New pages adopt it on creation.
- A master ref can point to any author's master event, enabling
reuse: ship one "Body pages" master and many books reference it.
---
## Phased delivery
### Phase A — Spec + Book mode skeleton
- New `templateMode: 'book'` with `state.book = { spec, cover, masters,
pages }`. Default spec: US Trade 6×9, 0.125 bleed, 0 pages.
- New `templateMode: 'book'` with `state.book = { spec, cover: null,
masters: [], pages: [] }`. Default spec: US Trade 6×9, 0.125 bleed,
0 pages.
- Sidebar Template tab gets a "Book" entry alongside Cover / Disc /
Custom Art / Slide / Deck.
- Empty Pages overview view (deck sorter clone, no pages yet) and
Cover view (cover designer in spec-aware mode, spine derived).
- Document settings modal with all spec fields wired.
- Save / load / autosave: the `book` block round-trips alongside
existing modes; no Nostr publish yet.
existing modes; refs are stored but no Nostr publish yet.
### Phase B — Pages overview + per-page editor
- Add / duplicate / delete / drag-reorder pages.
- Per-page editor (Custom Art canvas bound to `book-page:<id>`).
- Add / duplicate / delete / drag-reorder pages. Pages exist as
in-memory objects with provisional ids; not yet published.
- Per-page editor (Custom Art canvas bound to `book-page` target).
- Page-context bar (prev / next / Back to Pages).
- Page numbering live in thumbnails + the editor header.
- Page numbering live in thumbnails + the editor header. Token
substitution implemented for `{page-number}` / `{page-count}` /
`{physical-page-count}` / `{book-title}` / `{book-author}`.
### Phase C — Master pages
- Masters modal, master-edit mode in the canvas, master-layer underlay
in the page editor.
- `{page-number}`, `{page-count}`, `{book-title}` token substitution.
- Masters modal, master-edit mode in the canvas, master-layer
underlay in the page editor.
- Default master applied to new pages.
- Masters still in-memory, not yet published.
### Phase D — Nostr publish + fork
- `casewrap-book` Nostr publish path. `d`-tag `book:<title|id>`.
- Fork in the community browser (clone the whole book under a new
identity).
### Phase D1 — Publish individual page / cover / master events
- Per-page autosave publishes a `casewrap-page` event. Provisional
ids are upgraded to real `naddr` refs on first publish.
- Cover save publishes its own `casewrap-page` event with
`role: 'cover-spread'`.
- Master save publishes a `casewrap-page` event with `role: 'master'`.
- Linked Designs resolver wired so opening a book on a second client
/ device fetches the referenced events progressively.
- Disk cache: page / master / cover events cached in localStorage so
re-opens are instant offline.
- Upstream-update notifications surface on the sorter tiles +
cover-view header when a referenced event has a newer version
upstream.
### Phase D2 — Publish the manifest
- Manifest publish path: `casewrap-book` event with the spec + list
of `naddr` refs. Manifest is dirty whenever spec / page order /
page list / per-page-manifest-fields change.
- Fork (community browser): clones the manifest under the user's
pubkey; referenced pages stay as upstream refs unless the user
explicitly "Make local copy" on a per-page basis.
- Attribution: manifest p-tags every author that contributed a
referenced event (Linked Designs Phase 3a).
- Feed-card preview shows the cover spread thumbnail + page count.
### Phase E — PDF export
@@ -464,6 +691,7 @@ Tokens render at the live value in the page editor preview too.
- Export modal in Pages + Cover toolbars.
- Bleed + crop marks options. Single-file and split (cover + interior)
output modes.
- Gated-page export rules enforced.
### Phase F — Templates + import polish
- New `book` template category for the community browser; templates
@@ -471,15 +699,38 @@ Tokens render at the live value in the page editor preview too.
- Import a deck → convert to a book of N pages, each page seeded with
one slide's layers (cross-aspect letterbox-fit). Letting users
bootstrap a book from a deck is the cleanest reuse story.
- Import a single page from another book.
- Import a single page from another book (already works via Phase
D1's Linked Designs plumbing — formalise the UI).
### Phase G — Reader Mode + embed
- Full-screen Reader Mode with TOC sidebar, page-flip nav, progressive
fetch, premium-gate handling, NIP-57 zap inline.
- EXTERNAL_EMBED `?book=<naddr>` mode — flip-book in the iframe.
- Per-page NIP-25 reactions + kind-1 comments surfaced in the reader
(already work natively because pages are addressable; this phase is
just UX surfacing).
### Phase H — Premium / gating polish
- Per-page paywall toggle in the page editor. Manifest carries the
`gating` hint; the page event carries the actual encrypted payload.
- Whole-book paywall via gating the manifest itself.
- "Unlock with zap" flow tested end-to-end in Reader Mode.
---
## Risks & tradeoffs
- **Event size.** A book with 200 pages of layered art could easily
exceed a 256 KB Nostr event. v1 caps publish size and warns; v2
candidate: per-page sidecar events (à la Linked Designs).
- **Many small events instead of one big event.** Fetching 200 page
events is 200 relay round-trips' worth of overhead vs one fat
download. Mitigation: the Linked Designs batched-query resolver
(Phase 3d) already coalesces refs into bulk filters; localStorage
disk cache keeps re-opens instant; Reader Mode prefetches the next
two pages aggressively.
- **Manifest atomicity.** Manifest publish and per-page publish are
not transactionally linked. If a user republishes a page and
reorders pages in the same session, the manifest can land before
the page event reaches all relays. Mitigate with: publish pages
first, then manifest; show a "syncing" indicator until both land.
- **Master-layer perf.** Rendering master + page on every page-view
switch is 2× the layer work. Acceptable in v1 with the existing
renderer; if it bites, cache rasterised master snapshots.
@@ -489,8 +740,13 @@ Tokens render at the live value in the page editor preview too.
explicit, and a "test page" download for proofing a single page
before exporting the whole book.
- **Trim-resize collateral.** Resizing trim mid-design leaves layers
out of bounds. v1 warns but doesn't auto-scale. A "Refit selection
to trim" tool can come later.
out of bounds on pages that snapshotted the old trim. v1 warns and
offers per-page re-snapshot; no auto-scale.
- **Reference rot.** A referenced master / page event's author can
delete it (kind-5 tombstone) and the book breaks. Mitigation: the
manifest's "Make local copy" action lifts a ref into a copy under
the current user's pubkey. Encouraged at publish time for refs
that aren't your own.
- **Two-level navigation.** Pages → Page editor → back to Pages — the
user has to remember where they are. The page-context bar with
prev / next / "Back to Pages" is essential and should never be
@@ -506,16 +762,24 @@ Tokens render at the live value in the page editor preview too.
- **Bleed in the per-page editor.** Show as a guide always, or
toggleable? Cover designer shows it; we should match.
- **Per-page background image** vs masters owning the background.
Currently the spec puts `background` on both `book.pages[]` and
Currently the spec puts `background` on both the page event and
masters. Resolve precedence: page beats master beats spec default.
- **TOC generation.** Out of scope for v1, but a manual TOC page can
use the `{page-count}` token and a list of layer-text entries. A
`{toc}` magic token that auto-builds from page names would be a
natural Phase G addition.
- **Multi-book event size strategy.** When (not if) we hit relay
limits, do we prefer per-page sidecars (lots of small events,
parallel fetch) or a chunked-blob event (one big event split into
CBOR chunks)?
`{toc}` magic token that auto-builds from page labels would be a
natural Phase G+ addition.
- **Manifest event size at the extreme.** A 1,000-page book's
manifest is 1,000 refs × ~80 bytes each = ~80 KB. Approaching relay
limits. If we ever hit them, the manifest itself can be sharded
(chapter-manifests referenced from a top-level book event).
- **NIP-89 / client recommendations.** Worth publishing a kind-31990
recommendation so other Nostr clients know to open
`casewrap-book` / `casewrap-page` events with Artstr Studio. v2
unless someone else builds a competing book viewer first.
- **Linked Designs interaction.** Pages can themselves use Linked
Design layers (a page that pulls in another author's illustration).
Confirm this works through the renderer's existing async resolver
path — should "just work" but worth a smoke test in Phase B.
---
@@ -526,8 +790,8 @@ Tokens render at the live value in the page editor preview too.
- [ ] Document settings modal applies every spec field and the cover
view's spine width updates live when `paperThickness` or page count
changes.
- [ ] Cover view renders empty front / back / spine surfaces sized to
the spec.
- [ ] Cover view renders an empty front / back / spine surfaces sized
to the spec.
- [ ] Pages overview view renders an empty grid with an Add Page button.
### Phase B — Pages overview + per-page editor
@@ -535,18 +799,32 @@ Tokens render at the live value in the page editor preview too.
`book.pages` and the numbering reflects the new order.
- [ ] Clicking a tile opens the per-page editor; prev / next walk the
page list.
- [ ] Layers added in the editor are bound to `book-page:<id>` and
persist across reloads.
- [ ] Layers added in the editor are bound to `book-page` and persist
across reloads.
- [ ] `{page-number}` / `{page-count}` / `{physical-page-count}` /
`{book-title}` / `{book-author}` tokens substitute correctly in
text layers (live in the editor + in thumbnails).
### Phase C — Master pages
- [ ] At least one master exists; pages inheriting it render the
master layers below their own.
- [ ] `{page-number}` substitution renders the right number on each
page (matching `pageNumbering` config) in both the editor preview
and the overview thumbnails.
- [ ] Master-layer underlay is non-interactive in the page editor
(hover shows an "M" badge, click passes through).
### Phase D — Publish
- [ ] A book publishes as `casewrap-book` and reopens losslessly.
### Phase D1 — Per-event publish
- [ ] Saving a page publishes a `casewrap-page` kind-30078 event
under the user's pubkey.
- [ ] Reopening a book on a second device resolves every page /
master / cover ref via the Linked Designs resolver.
- [ ] An upstream master update surfaces a "new version available"
notice in the masters modal and (when accepted) propagates to every
page that inherits it.
### Phase D2 — Manifest publish
- [ ] A book publishes as `casewrap-book` and reopens losslessly
across devices.
- [ ] Manifest p-tags every author of every referenced event
(attribution chain).
- [ ] Community-browser feed-card shows the cover thumbnail + page
count.
@@ -556,3 +834,18 @@ Tokens render at the live value in the page editor preview too.
- [ ] Bleed + crop-marks toggle work; output passes a visual diff
against a hand-laid InDesign export of the same book at a single
reference size.
- [ ] A premium-gated page included in the export is replaced by the
watermarked preview when the user is not authorised to unlock it.
### Phase G — Reader Mode + embed
- [ ] Reader Mode walks the manifest's pages with progressive fetch;
prev / next page navigation works keyboard + touch + click.
- [ ] EXTERNAL_EMBED `?book=<naddr>` opens the same book in an iframe
flip-book.
- [ ] A gated page in Reader Mode shows the watermarked preview and a
working "Unlock with zap" CTA.
### Phase H — Premium gating
- [ ] A book with one gated chapter publishes; an unauthorised reader
sees the preview; a reader who zaps to unlock sees the real content
in Reader Mode without re-opening the book.