Files
Artstr-Studio/docs/BOOK_DESIGNER_FEATURE.md
thePR0M3TH3AN 2f5167054b docs(book): polish pass — schema consistency + MVP path + missing acceptance
Final spec polish after the markdown / NIP-23 revision.

Schema consistency:
- Manifest entries now carry a stable opaque `id` (generated at
  insert time). Used for anchoring designed pages around reflow
  segments — anchors reference `entryId` instead of fragile array
  positions or human-readable strings.
- Unified `masterRef` across designed and reflow entries (was
  `inheritsMasterRef` on designed, `masterRef` on reflow). Both
  default to `spec.defaultMasterRef` when null.
- Promoted `AnchorBlock`, `GatingBlock`, `TextStylesBlock`,
  `TypographyObject` to named definitions in the data-model block
  so the schema is documented once and referenced from prose.
- Page event `d`-tag pinned to a stable opaque id, not derived from
  the page title (titles change; d-tags shouldn't).
- Added uniqueness rules: a chapter or page event can appear in at
  most one manifest entry per book; every entry has a unique `id`.

New sections:
- **Chapter event example** (kind-30023 NIP-23 article) so the
  full event shape is documented alongside page / master / cover.
- **Schema + compatibility** — bump to SCHEMA_VERSION 6,
  additive on the project payload, with the documented fallback
  for older clients.
- **Privacy** — confirms PRIVATE_PUBLISHING works across all
  constituent events of a book.
- **Compatibility notes** — Linked Designs inside pages works
  via existing async resolver (worth a Phase B smoke test); all
  existing layer types render on book targets.
- **MVP launch path** — two reasonable launch targets:
  MVP-A (designed-only books, Phases A+B+D1+D2) and MVP-B
  (books with prose, MVP-A + C+C.5+E). Phases F/G/H/I are strict
  enhancements that don't block either MVP.

Tightened semantics:
- **Token substitution scope** explicitly limited to (a) text
  layers and (b) reflow-entry display fields. Markdown bodies of
  chapter events stay portable to other NIP-23 clients — no
  book-specific tokens leak in.
- PDF section: removed outdated "may flatten text to vector
  paths" notes — the font allowlist + per-export warning is now
  the committed approach. Reflowed prose and designed-page text
  emit as real PDF text uniformly.
- PDF outline / bookmarks moved out of "limitations" — they ship
  in Phase I built from `chapterTitle` and `{toc}` targets.

Cleaned Open Questions: removed answered items (TOC token is now
Phase I, Linked Designs interaction moved to Compatibility notes).

Added missing acceptance criteria for Phase F (templates +
deck-to-book import) and Phase I (CodeMirror + extended subset +
auto-TOC + cross-references).
2026-06-02 10:53:53 -04:00

69 KiB
Raw Permalink Blame History

Book Designer

Goals

A unified document type for designing books — a cross between InDesign 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 automatically (e.g. adding pages re-computes spine width).

The three existing workflows are the building blocks:

  • Cover designer → renders the book's front / back / spine spread.
  • Slide-deck overview → reorganises pages and gives a thumbnail-grid table of contents.
  • Custom-art canvas → the per-page design surface.

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 Nostr-native book, not a monolithic blob. The manifest event carries the spec + an ordered list of naddr refs. Each page, each master, the cover, and each prose chapter (as a NIP-23 article) are separate addressable events. Edits to one event replace only that 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. 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.
  11. Reflowable text via Markdown + NIP-23. Prose-heavy chapters are written as markdown and published as NIP-23 long-form articles (kind-30023). The book manifest references each chapter's article event and pours its markdown into per-page text frames at render time using a book-level stylesheet (H1 / H2 / H3 / body / blockquote / code / link typography). Designed pages and reflow segments interleave in the page sequence. The same NIP-23 article can be read standalone on Habla.news and presented as a designed chapter inside a book — one source, two presentations.

Non-goals (v1)

  • Reflowable text beyond the v1 markdown subset. v1 supports paragraphs, h1-h6, bold / italic / inline code, code blocks, lists (ordered + unordered), blockquotes, links, hr, and inline images (one per line). Out of scope for v1: tables, footnotes / endnotes, definition lists, GFM task lists, embedded HTML, multi-column text frames, float-around-image, ligatures beyond the renderer's defaults. Each deferred feature can land later as its own phase with its own typography rules.
  • Threaded text frames across non-adjacent pages. Reflow always fills frames in master-page order; you can't manually link a frame on page 12 to one on page 47.
  • A WYSIWYG markdown editor. v1 ships a textarea + live preview. Rich syntax-highlighted editing is a Phase I follow-up.
  • Auto-paginated tables of contents, indices, cross-references. All of these depend on a richer flow model. Manual TOC pages are fine; a {toc} magic token that auto-builds from chapter titles is on the open-questions list.
  • CMYK colour management or ICC profile attachment. PDF export is sRGB. Pro-print colour fidelity is a phase E+ concern.
  • Imposition / signatures / booklet print layouts. Pages export in reading order; if a printer needs imposition they'll handle it themselves.
  • 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.
  • 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

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 + 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 / 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 + 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.
  • NIP-23 long-form articles are already first-class Nostr citizens with rich tooling (Habla.news, Yakihonne, others). A book chapter is a NIP-23 article event — we reuse the kind, the editor ecosystem, and the discovery surface for free. Authors can write on whatever NIP-23 client they prefer and assemble the book later.
  • Nostr publish + fork + embed. Books reuse the existing publish pipeline; each constituent event reuses the same kind-30078 with new tag names.

What's actually new:

  • state.book as a manifest plus a hydrated cache of referenced page / master / cover / chapter 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 as a new event type with a single-level inheritance render pass.
  • text-frame layer type on masters — a rectangular flow region the pagination engine fills with markdown content.
  • Pagination engine. Markdown tokens → measured lines → page breaks, with widow / orphan rules to avoid bad page splits. Powers both on-screen render and PDF export.
  • Book-level text stylesheet (spec.textStyles) that maps each markdown element to typography (font, size, weight, leading, alignment, top/bottom margin).
  • Markdown editor surface in the page editor for reflow entries — a textarea with live preview of the rendered chapter, scrolled in sync with the source.
  • 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 — with reflowed prose as real PDF text (searchable, copyable, not flattened-to-paths) where possible.
  • 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.

Framing within Template / Designer

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 / 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, which hydrates page / master / cover refs from Nostr via the Linked Designs resolver.


Data model

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:

{ kind: 30078, pubkey: '<hex>', dTag: '<d-tag>' }

Convertible to / from naddr at the UI layer.

Book manifest (templateMode: 'book', tag casewrap-book)

{
  version: SCHEMA_VERSION,
  templateMode: 'book',
  templateType: 'book',
  meta: { … },                                  // title / category / language / tags
  book: {
    spec: {
      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 → 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,                    // index into `pages` — skip cover, title, etc.
        format: '{n}',                           // '{n}' / 'Page {n}' / '— {n} —'
      },
      bookSize: 'US Trade 6×9',                  // label matching a preset
      defaultMasterRef: { … } | null,            // applied to new pages on creation
      textStyles: {                              // book-level markdown stylesheet (see "Reflowable text" below)
        body:       { fontFamily: 'Georgia', fontSize: 11, lineHeight: 1.5, color: '#111', marginAfter: 0.08, alignment: 'left' },
        h1:         { fontFamily: 'Helvetica', fontSize: 24, fontWeight: 700, marginBefore: 0.4, marginAfter: 0.2, alignment: 'center', keepWithNext: true },
        h2:         { fontFamily: 'Helvetica', fontSize: 18, fontWeight: 700, marginBefore: 0.3, marginAfter: 0.15, keepWithNext: true },
        h3:         { fontFamily: 'Helvetica', fontSize: 14, fontWeight: 700, marginBefore: 0.2, marginAfter: 0.1, keepWithNext: true },
        blockquote: { fontStyle: 'italic', marginLeft: 0.3, marginRight: 0.3 },
        codeBlock:  { fontFamily: 'monospace', fontSize: 10, background: '#f5f5f5', padding: 0.05 },
        inlineCode: { fontFamily: 'monospace', fontSize: 10, background: '#f5f5f5' },
        link:       { color: '#0066cc', underline: true },
        list:       { marginAfter: 0.08, indent: 0.2 },
        hr:         { color: '#cccccc', thickness: 0.5, marginBefore: 0.15, marginAfter: 0.15 },
      },
    },
    cover: { ref: { kind, pubkey, dTag } },      // a `casewrap-page` with role:'cover-spread'
    pages: [                                     // discriminated union; see GatingBlock + AnchorBlock below
      // 1) Designed page (fixed-layout):
      {
        id: 'entry:abc123',                      // stable opaque id, generated on insert — used by AnchorBlock and the editor's "which entry am I on"
        type: 'design',
        ref: { kind, pubkey, dTag },             // a `casewrap-page` with role:'interior'
        label: 'Title page',                     // optional sorter label override
        masterRef: { kind, pubkey, dTag } | null,// overrides spec.defaultMasterRef; null = use default
        skipNumber: false,                       // true = excluded from {page-number} sequence
        anchor: AnchorBlock | null,              // see below; null = "sits after the previous entry"
        gating: GatingBlock | null,
      },
      // 2) Reflow segment (markdown text poured into N pages):
      {
        id: 'entry:def456',
        type: 'reflow',
        textRef: { kind: 30023, pubkey, dTag },  // a NIP-23 article event carrying the chapter's markdown
        textPinnedEventId: '<hex>' | null,       // when set, lock to this exact revision (event id ref). null = always use latest addressable.
        masterRef: { kind, pubkey, dTag },       // master whose text-frame layer the markdown flows into; required, must reference a master with a text-frame layer
        chapterTitle: 'Chapter 3: The Threshold',// overrides article's title for {chapter-title} tokens
        chapterNumber: 3 | null,                 // explicit number for {chapter-number}; null = auto-numbered by reflow-segment ordinal
        startsOnPage: 'recto' | 'verso' | 'any', // bookbinding: chapter starts on right-hand page = 'recto'
        skipNumberOnFirstPage: false,            // common print convention: page number suppressed on a chapter's first page
        styleOverrides: TextStylesBlock | null,  // per-chapter overrides to spec.textStyles (rare)
        gating: GatingBlock | null,              // gates the textRef event in PREMIUM_DESIGNS pipeline
      },
      …                                          // ordered list, variable length
    ],
    masters: [                                   // refs the book wants surfaced in the Masters modal. A master ref used elsewhere (e.g. via masterRef on a page) doesn't have to appear here, but listing it makes it pickable from the UI.
      { ref: { kind, pubkey, dTag }, label: 'Body pages' },
      …
    ],
  },
}

Where:

// AnchorBlock: keeps a designed page's semantic position stable as
// reflow segments expand or contract. Without an anchor, a designed
// page sits at its array position — which can be ambiguous when a
// chapter ahead of it changes length.
AnchorBlock =
  | { mode: 'before-chapter',  entryId: 'entry:def456' }   // landed just before the named reflow entry
  | { mode: 'after-chapter',   entryId: 'entry:def456' }   // landed just after the named reflow entry's last paginated page
  | { mode: 'at-position',     after: 'entry:abc123' };    // sits right after the named designed entry (chains designed pages explicitly)

// GatingBlock: hint that this entry is paid content. Authoritative
// crypto state lives on the gated event itself; clients verify by
// reading the event, not by trusting the manifest.
GatingBlock = {
  mode: 'free' | 'premium',
  previewWatermark: true,                        // PREMIUM_DESIGNS pipeline renders watermarked preview when locked
  priceSats: 1000 | null,                        // hint only; truth is on the event
};

// TextStylesBlock: a partial map of element-name → typography
// object. Used in three places: book.spec.textStyles (the
// stylesheet), reflow entry's styleOverrides, and the master's
// text-frame.styleOverrides. Merged low-to-high precedence at
// render time.
TextStylesBlock = {
  body: TypographyObject,
  h1: TypographyObject, h2: …, h3: …, h4: …, h5: …, h6: …,
  blockquote: TypographyObject,
  codeBlock: TypographyObject, inlineCode: TypographyObject,
  link: TypographyObject,
  list: TypographyObject,
  hr: TypographyObject,
};

// TypographyObject keys (all optional):
// { fontFamily, fontSize, fontWeight, fontStyle, lineHeight, color,
//   alignment, marginBefore, marginAfter, marginLeft, marginRight,
//   underline, background, padding, indent, thickness,
//   keepWithNext, keepTogether }

Page event (kind-30078, tag casewrap-page)

{
  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-*
  },
}

Notes on the page event

  • 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 — a stable opaque id (e.g. book-page:abc123) generated at insert time, NOT derived from the page's title (titles change, d-tags shouldn't). 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 snapshot the spec at publish time so a stand-alone preview is sane. A master intended for reflow MUST carry exactly one text-frame layer (the manifest UI refuses to pair a master without one). A master without a text-frame is still valid for designed pages — it just doesn't participate in reflow.

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.

Chapter event (kind-30023 NIP-23 article)

Vanilla NIP-23. The book references it via the reflow entry's textRef. The article has no book-specific schema; it's a normal long-form article and opens cleanly on Habla, Yakihonne, or any other NIP-23 client.

{
  kind: 30023,
  pubkey: '<hex>',
  created_at: <unix>,
  tags: [
    ['d', 'chapter:<slug>'],                     // standard NIP-23 d-tag for replaceable addressing
    ['title', 'Chapter 3: The Threshold'],
    ['summary', '<optional short summary>'],
    ['published_at', '<unix>'],                  // when first published (NIP-23 convention)
    ['t', 'book-chapter'],                       // optional, for discoverability in book-aware clients
    // …any other normal NIP-23 tags (language, image, etc.)
  ],
  content: '<markdown body>',
}

A book can reference any author's existing NIP-23 article as a chapter — no special encoding required. Conversely, a chapter written for a book is a perfectly normal article that any NIP-23 reader can open.

Uniqueness rules

  • A given chapter event (by textRef) may appear in at most one reflow entry per book. The Pages overview refuses to add a duplicate; if a user wants two appearances they must duplicate the chapter event under a new d-tag first.
  • A given designed-page event (by ref) may appear at most once per manifest. Same rule.
  • Every manifest entry has a unique id. Generated at insert time; stays stable across reorders so anchors don't break.

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 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 / chapter (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.
  • Chapter events are vanilla NIP-23. A book chapter is just a kind-30023 article — same kind Habla.news and Yakihonne publish. No book-specific tag is required, though we'll add an optional t tag book-chapter for discoverability. A book can reference any author's existing NIP-23 article as a chapter, and any NIP-23 client can open a referenced chapter standalone.

Schema + compatibility

  • The book feature bumps SCHEMA_VERSION to 6 (current is 5).
  • The new book block on the project payload is additive; older clients that load a v6 project either skip Book mode entirely or fall back to a "Unsupported template — open in latest Artstr" notice (same fallback PIXEL_ART used at its v5 bump).
  • Page, master, and cover events use templateMode: 'book-page' inside their payload (one shape, three roles).
  • Chapter events have no templateMode — they're raw NIP-23 articles, opaque to the book code beyond their content and metadata tags.
  • Old clients seeing a casewrap-book manifest can render the feed-card thumbnail (the cover ref resolves to a normal casewrap-page event they understand visually); only opening the full editor requires v6.

Privacy

Books can be published privately via the existing PRIVATE_PUBLISHING flow: every constituent event (manifest, pages, masters, cover, chapters) can be encrypted to the author's own pubkey or shared with a specific reader set. Per-event privacy is already the model, so nothing new is needed beyond making sure the publish UI offers the choice at the right granularity (default: public; opt-in to private at publish time, applied uniformly across all constituent events of that publish action).


Token substitution + page-count semantics

Magic tokens that resolve at render time using the manifest's context. Tokens are honored in two places only:

  1. The HTML of a regular text layer on a designed page or a master.
  2. Reflow entry fields that drive on-canvas display (chapterTitle, sorter labels).

Tokens are not substituted inside a chapter event's markdown body — chapter events are vanilla NIP-23 articles that must remain portable to other long-form clients. If you want a chapter title in a running header, put a {chapter-title} token on a text layer on the chapter's reflow master, not in the markdown.

  • {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 after reflow pagination, including skipped ones. Rarely useful but exposed for completeness.
  • {book-title} → manifest meta.title.
  • {book-author} → manifest pubkey resolved to a profile name.
  • {chapter-title} → the current reflow segment's chapterTitle, or the NIP-23 article's title when none is overridden. Useful for running headers on master pages — the value updates as the reader moves through chapters.
  • {chapter-number} → the current reflow segment's chapterNumber, or its 1-based ordinal among reflow segments if not explicit.

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]).


Reflowable text + Markdown

Prose chapters are stored as NIP-23 long-form articles (kind-30023, markdown body). A reflow entry on the book manifest references the chapter event and pairs it with a master that has a text-frame layer; at render time the pagination engine pours the chapter's markdown into copies of the master until the chapter is exhausted. Designed pages and reflow entries interleave in the page sequence.

Why markdown

  • Markdown encodes paragraph + character styles without inventing a schema. # H1, ## H2, **bold**, > blockquote, ``` ``code``, lists, links, images. The book'sspec.textStyles` defines how each element renders — that's the entire "paragraph styles" feature.
  • One source, many presentations. An author writes a chapter on Habla.news. It's published as a kind-30023 article. Readers on Habla see a clean reading view. The author later assembles a book on Artstr Studio that references the same article event; readers on Artstr see it laid out as designed book pages. Edit on Habla → the next book read picks up the change.
  • Event size fits. A typical chapter is 3-5K words ≈ 20-30 KB of markdown. Well within relay event limits. A 100K-word novel splits cleanly into ~30 chapter events.

text-frame layer type (new)

A new shape-like layer that masters can carry. Has:

  • x / y / w / h / rotate (same as any layer)
  • padding (inside the frame, inches)
  • verticalAlign: top | middle | bottom
  • columns: 1 in v1 (multi-column deferred)
  • styleOverrides: optional partial textStyles block; merges over the manifest's spec.textStyles for prose rendered into this frame only

A master with no text-frame is invalid for use as a reflow master (the manifest UI refuses the pairing). A master with text-frame can still be used for designed pages — the frame just doesn't get filled.

spec.textStyles (book-level stylesheet)

Maps each markdown element to typography. Inheritance order at render time, low → high precedence:

  1. Built-in defaults (sane fallbacks per element)
  2. book.spec.textStyles
  3. Reflow entry's styleOverrides (per chapter)
  4. Master's text-frame.styleOverrides (per frame)

Each style block is a plain typography object — same fields the existing text layer already speaks: fontFamily, fontSize, fontWeight, fontStyle, lineHeight, color, alignment, marginBefore, marginAfter, marginLeft, marginRight, plus boolean flow controls (keepWithNext, keepTogether) for the pagination engine.

Markdown subset (v1)

Supported:

  • Paragraphs (blank-line separated)
  • # H1 through ###### H6
  • **bold**, *italic*, ***bold-italic***, ~~strikethrough~~
  • `inline code`, fenced code blocks (```)
  • > blockquote (single-level)
  • Ordered (1.) and unordered (-, *) lists, single level
  • [link text](url) — links render with the link style; URL is preserved as a layer attribute for PDF export
  • ![alt](url) inline images — one per line, full-width-of-frame, with optional alt-text caption underneath (per stylesheet config). URLs may be nostr: (resolves to a NIP-94 file event) or https:
  • --- horizontal rule
  • Soft line break (two trailing spaces): renders as <br>
  • Hard line break (blank line): paragraph break

Deferred (Phase I+):

  • Tables (own typography rules + layout pass)
  • Footnotes / endnotes (need a footnote-frame layer and per-page collection)
  • Definition lists, GFM task lists
  • Nested lists beyond one level
  • HTML embedded in markdown (security + complexity)
  • Multi-column text frames
  • Float-around-image (text wraps around an inline figure)

Pagination engine

Pure function: takes (markdown, frame geometry, stylesheet, master, page-context) and returns ({ pageBreaks, lineLayout, overflow }).

  1. Parse markdown into a token stream using marked (~30 KB minified, MIT, vendored as src/vendor/marked.min.js). We use the lexer output directly, not the HTML renderer — we want token shapes for measurement, not HTML strings.
  2. Measure each token against the current frame's available width. Text wrapping uses a Canvas2D measurement context with the right font / size loaded.
  3. Line-break greedily within paragraphs (no Knuth–Plass in v1).
  4. Page-break when a line doesn't fit, with these rules:
    • keepWithNext: true on a heading style → if the heading would be the last line on the page, push it to the next page too.
    • keepTogether: true on a small block (e.g. an image with its caption) → don't split it across pages.
    • Widow rule: a paragraph's last line should not stand alone at the top of the next page. If it would, push the previous line too.
    • Orphan rule: a paragraph's first line should not stand alone at the bottom of the previous page. If it would, push the heading too.
  5. Generate virtual page entries (master + filled text-frame) that feed into renderLayers exactly like designed pages.

Token substitution ({chapter-title} etc.) happens at the text-frame's containing-page level, not at the token-stream level — the engine emits placeholder tokens and the page renderer substitutes on draw.

Edge cases v1 documents (and does not fix):

  • A word wider than the frame: render with horizontal overflow + a warning toast.
  • A code block wider than the frame: same — no auto-hyphenation, no shrink-to-fit.
  • An image wider than the frame: scale down to frame width.
  • A page that has zero room (the master's text-frame has negative effective height due to padding overflow): refuse and surface an error.

Anchoring designed pages around reflow segments

A reflow segment expands to N pages at render time, so designed pages can't be addressed by absolute page index — N changes when prose changes. Each designed page on the manifest carries an anchor field that references another manifest entry's stable id so the relationship survives reorders and reflow:

{ type: 'design', id: 'entry:dedication',     ref: …, anchor: { mode: 'before-chapter', entryId: 'entry:chapter-1' } }
{ type: 'design', id: 'entry:epilogue',       ref: …, anchor: { mode: 'after-chapter',  entryId: 'entry:chapter-12' } }
{ type: 'design', id: 'entry:half-title',     ref: …, anchor: { mode: 'at-position',    after: 'entry:title-page' } }
{ type: 'design', id: 'entry:title-page',     ref: …, anchor: null }   // null = "sits at its array position"

The pagination pipeline interleaves anchored designed pages around reflow segments so the semantic order is stable even as page counts shift. A new designed page is given anchor: null by default (sits at insertion order); the user can promote it to a chapter anchor from the Pages overview's per-tile menu.

Pinning chapter revisions

Text drift is the biggest reflow tradeoff: if an author edits the NIP-23 article a book references, the book's pagination changes — designed pages may shift, the printed page count may differ from what was proofed. Two modes per reflow segment:

  • Pinned (textPinnedEventId set): the manifest locks to a specific event id. kind-5 deletes or addressable replacements upstream don't affect this book. Updates are explicit: an "Update pinned chapter" command pulls the latest revision and re-runs pagination.
  • Live (textPinnedEventId null): the manifest tracks the addressable ref — always the latest. Linked Designs' upstream- update notification surfaces when the chapter changes, and the reader prompts to refresh.

Default: pinned at publish time. Safe by default for print runs. Authors can opt a chapter into live mode if they want "latest-always" behaviour (e.g. for a serialised work in progress).

Markdown editor surface

For each reflow entry, the page editor switches to a two-pane layout:

  • Left pane: markdown textarea (autosaves to the chapter's local draft; explicit "Save chapter" publishes a new revision of the NIP-23 event).
  • Right pane: live preview rendered through the same pagination engine the book will use, showing the chapter as paginated pages with the selected master applied.

v1 ships a plain textarea + preview; a syntax-highlighted CodeMirror upgrade is a Phase I follow-up.

The editor also exposes:

  • A "Import from Nostr" field that paste-fetches an existing kind-30023 article event and seeds the textarea with its content.
  • A "Pin / Live" toggle.
  • A "Re-paginate now" button (shows page count + warns about changes since last paginate).

The surfaces

1. Document settings modal

Owns the spec. Accessible from a "Document settings" button in every book view. Fields:

  • Book size preset: US Trade 6×9, US Letter 8.5×11, A5 5.83×8.27, A4 8.27×11.69, Square 8×8, Custom. Picking a preset fills trim width/height + sensible default bleed and paper-thickness; switching to Custom unlocks them.
  • Trim width / Trim height (inches, numeric).
  • Bleed (inches, numeric, default 0.125).
  • Paper thickness (inches per page) + Spine from pages? checkbox. When checked, spine width is derived; when unchecked, a manual Spine width field appears.
  • 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 — "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, 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 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 three counters: Numbered: N, Physical: M (after reflow pagination), Reflow segments: K.
  • Two tile styles by entry type:
    • Designed-page tile: a thumbnail of the rendered page, just like the deck sorter shows slides today.
    • Reflow tile: a stack-of-pages icon with the chapter title, chapter number, a master-name pill, the latest paginated page count (e.g. "Chapter 3 — Body pages master — 14 pages"), and a tiny preview thumbnail of the chapter's first paginated page.
  • Drag-reorder writes back to book.pages[]. Numbering and reflow re-paginate live; the manifest event is marked dirty for republish.
  • Per-tile menu — designed: Edit, Duplicate, Delete (removes from manifest; the page event itself is left untouched on relays), Apply master…, Skip numbering, Rename, Open standalone.
  • Per-tile menu — reflow: Edit chapter (opens the markdown editor), Change master…, Pin / Live toggle, Re-paginate, Open chapter on Habla (link to the NIP-23 article on a public long-form client for cross-checking), Delete from book.
  • A toolbar above the grid: + Add designed page, + Add chapter (creates a new draft NIP-23 article + a reflow entry referencing it), Import chapter from Nostr (paste an existing kind-30023 article id; manifest gets a ref to it without republishing), Import designed page from Nostr, Manage masters…, Document settings.

4. Per-page editor

The existing Custom Art canvas with three wraps:

  • A page-context bar at the top of the canvas: Page 12 / 248, prev / next arrows (← / →), a master-page badge ("Inheriting: Body pages"), and a "Back to Pages" button.
  • A master layer underlay: master layers render under the page's own layers, with reduced opacity (~50 %) and a small "M" badge when a master layer is hovered, to make the inheritance obvious. Master layers are not editable from the page editor — they're click- through to the page layer below. To edit a master, the user opens it from the masters modal (which routes back to the same canvas in master-edit mode — the only difference is which target the editor binds new layers to).
  • A trim + bleed guides overlay (already exists in cover designer conceptually). Trim is dashed red, bleed is dashed teal, optional margin / gutter from the spec is faint grey.

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 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 / 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 (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.


PDF export

What v1 produces

  • One PDF with N + (1 or 2) pages: every interior page in order, then the cover spread on its own landscape page. (Back / spine / front flow follows the cover designer's existing left-to-right layout.)
  • Page size: trimW × trimH plus bleed margin if "include bleed" is on. Cover page size: full cover-spread width × trim height plus bleed.
  • sRGB. No ICC embedding. Fonts on the embeddable allowlist (Georgia, Times, Helvetica, Arial, monospace family) are subset-embedded; layers using any other font fall back to vector paths and are listed in a per-export warning so authors can pick whether to swap fonts before re-exporting.
  • Crop marks toggle: thin black L-marks at each trim corner, offset outside the trim by the bleed amount.

Library

pdf-lib (~80 KB minified, MIT). Pure-JS, runs in the browser, supports images, SVG paths (via converting our d strings into PDF path ops), text, fonts, page management. Vendored as src/vendor/pdf-lib.min.js the same way polygon-clipping is.

Pipeline

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. 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 — both designed-page text layers and the pagination engine's reflowed prose — emit as real PDF text objects (vector, searchable, copyable) when the font is on the allowlist; otherwise fall back to vector paths with the per-export warning. This applies uniformly to both reflowed prose and designed-page text.
  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.

Export UI

A new "Export PDF…" button in the Pages overview toolbar and the Cover view toolbar opens an export modal:

  • Interior pages: All / range / current
  • Include cover spread: yes / no
  • Include crop marks: yes / no
  • Include bleed: yes / no
  • Output: Interior + Cover (single file) / Interior PDF + Cover PDF (two files). Print shops often want them separate.
  • Filename pattern.

Limitations baked in v1

  • Layers using fonts outside the embeddable allowlist fall back to vector-path text (still visually identical, just not selectable in the PDF). The per-export warning lists which layers were affected.
  • No PDF bookmarks / outline / TOC links. Phase I+ adds bookmarks built from each reflow segment's chapterTitle and the {toc} token's targets.
  • 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 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 {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-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

MVP launch path

Book mode is a large feature. The phase order is set up so each phase ships independently and the project never sits in a half-done state. Two reasonable launch targets:

  • MVP-A: Designed-only books. Phases A + B + D1 + D2. A user can lay out a fixed-layout book with cover + designed interior pages and publish it as a manifest + sidecar events. No reflow, no PDF, no Reader Mode. Useful for art books, zines, photo books, comic chapbooks.
  • MVP-B: Books with prose. MVP-A + Phase C + C.5 + E. Adds masters, reflowable chapters via NIP-23, and PDF export. This is the full "book designer" experience.

Phases F (templates + deck-to-book import), G (Reader Mode + embed), H (premium gating), and I (markdown + extras) are strict enhancements — none of them block MVP-A or MVP-B.

Phase A — Spec + Book mode skeleton

  • 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; refs are stored but no Nostr publish yet.

Phase B — Pages overview + per-page editor

  • 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. 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.
  • Default master applied to new pages.
  • text-frame layer type added — masters can carry a text-frame on the canvas (a new layer kind that renders as a labelled rectangle in the editor and is the future drop-target for prose). No reflow yet; the frame is purely structural.
  • Masters still in-memory, not yet published.

Phase C.5 — Reflowable text (markdown + pagination)

  • Vendor marked (~30 KB MIT) as src/vendor/marked.min.js.
  • Implement the pagination engine (token stream → measured lines → page breaks, with widow / orphan + keepWithNext / keepTogether). Pure-function shape so it's testable in isolation.
  • spec.textStyles editor in Document settings.
  • reflow-type entries in book.pages[]; pages overview tiles render reflow segments with the "stack of pages" affordance and paginated page-count.
  • Markdown editor pane in the per-page editor for reflow entries (textarea + live preview).
  • Anchoring of designed pages around reflow segments (anchor.mode = 'before-chapter' | 'after-chapter' | 'at-position').
  • {chapter-title} / {chapter-number} tokens substitute correctly in master text layers.
  • Pin / Live revision tracking for chapter refs.
  • text-frame layer is now functional — masters with one become valid reflow-master targets; masters without one are rejected by the "Set master" picker for reflow entries.
  • NIP-23 import: paste a kind-30023 article id, get a reflow entry that references it.

Phase D1 — Publish individual page / cover / master / chapter 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'.
  • Chapter save publishes a kind-30023 (NIP-23) article event. Title comes from the reflow entry's chapterTitle; body is the markdown source. The article event is independent — readable on any NIP-23 client.
  • Linked Designs resolver wired so opening a book on a second client / device fetches the referenced events progressively.
  • Disk cache: page / master / cover / chapter 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; for chapters, the notice also reports the new word count delta.

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

  • Vendor pdf-lib. Build the render-to-PDF pipeline.
  • Reflowed prose exports as real PDF text (not flattened paths) using pdf-lib's font subsetting. Designed-page text layers whose font isn't embeddable fall back to vector-path text with a per-export warning listing which layers were flattened.
  • Export modal in Pages + Cover toolbars.
  • Bleed + crop marks options. Single-file and split (cover + interior) output modes.
  • Gated-page export rules enforced.
  • A "Proof one page" quick-export emits a single page or a single chapter as a PDF — useful for sending to a printer to test colour / paper before exporting the full book.

Phase F — Templates + import polish

  • New book template category for the community browser; templates ship at common sizes (US Trade, A5, A4).
  • 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 (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.

Phase I — Markdown editor + extended subset

  • Replace the plain textarea with a syntax-highlighted CodeMirror instance (~150 KB minified, vendored). Adds inline rendering hints, smart quotes, list-continuation, drag-and-drop image insert.
  • Extended markdown features, each added with its own typography rules and pagination handling:
    • Tables with column-width balancing and per-cell alignment.
    • Footnotes — a footnote-frame layer on masters collects references per page; reference markers in the flow render as superscripts.
    • Nested lists beyond one level (typography per level).
    • Definition lists, GFM task lists.
  • Auto-generated TOC via {toc} magic token that walks every reflow segment's H1 / H2 / H3 headings and emits a styled list with page numbers.
  • Auto-generated index via {index} and inline ^[term] markers in the markdown source.
  • Cross-references: [See chapter 3](#chapter-3) resolves to a page number at render time.

Risks & tradeoffs

  • 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.
  • PDF correctness. This is the biggest correctness risk — print output that's slightly off can cost the user money at the printer. Mitigate with: clear preview before download, bleed / crop options 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 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 hidden.
  • Pagination is fiddly. Pagination engines have a long tail of edge cases — long words on narrow columns, code blocks wider than the frame, images that should keep with their caption, mid-line font changes. Mitigations:
    • Tight v1 subset. Document the supported markdown subset explicitly. Each deferred feature lands as a separate phase with its own tests.
    • Pure-function engine so it's testable in isolation: feed it a markdown string + frame geometry + stylesheet, assert page breaks. Build a small canonical corpus of "should paginate these ways" fixtures.
    • "Preview pagination" command before publish so the author sees the page count + any overflow warnings without committing.
    • Overflow warnings in the editor: a long word, a too-wide code block, a margin that swallows the frame — each emits a toast pointing at the offending line, rather than silently rendering wrong.
  • Text drift breaking pagination. A chapter event updates upstream → its word count changes → physical page count shifts → designed pages anchored after that chapter move. Mitigations:
    • Pin chapters at publish time by default. Authors who want "latest-always" opt-in to live mode per chapter.
    • Re-paginate notice when a live chapter changes, with a diff (+12 pages / -3 pages) shown in the sorter so the author knows what shifted before accepting the update.
    • Anchor-based designed pages. Designed pages on the manifest use anchor.mode: 'before-chapter' | 'after-chapter' rather than absolute index, so they stay semantically in place even when page counts shift.
  • PDF text fidelity for reflowed prose. Reflowed paragraphs must export as real PDF text objects (searchable, copyable) — the Phase E PDF pipeline uses pdf-lib font subsetting for this. Mitigations:
    • Font allowlist. Document settings restricts text-frame fonts to ones we know pdf-lib can subset cleanly (start with a small set of bundled web-safe fonts: Georgia, Times, Helvetica, Arial, monospace).
    • Fallback to vector paths for fonts outside the allowlist, with a per-export warning listing which paragraphs were flattened.
  • Markdown editor scope. Building a full WYSIWYG markdown editor would eat the v1 timeline. Mitigation: ship a plain textarea + live preview for v1 (Phase C.5). CodeMirror upgrade is Phase I. A WYSIWYG editor is explicitly never in scope — markdown is the source of truth, not a transient representation.

Open questions

  • Facing pages. Do we want a v1.5 facing-page preview (two pages side by side) even without facing-page editing? It's a small render addition; useful for previewing recto / verso bookbinding before print.
  • Bleed guide in the per-page editor. Show as a guide always, or toggleable? Cover designer shows it always; recommend matching for consistency unless user research surfaces a reason not to.
  • background precedence. Spec puts background on the page event, the master, and spec (implicitly). Proposed precedence: page event's background wins, then master's, then spec default (#ffffff). Lock this in code review of Phase B.
  • Manifest event size at the extreme. A 1,000-page book's manifest is ~1,000 refs × ~80 bytes = ~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). Not worth solving until a real user runs into it.
  • NIP-89 / client recommendations. Publishing a kind-31990 recommendation so other Nostr clients know to open casewrap-book / casewrap-page events with Artstr Studio is worth doing eventually but only matters once a competing viewer exists.
  • Inline-image source resolution. v1 markdown images accept https:// URLs. Adding nostr: resolution (image events under NIP-94, profile pictures by npub) is a small Phase C.5 follow-up worth scoping once the engine is working.

Compatibility notes

  • Linked Designs inside pages. A page can include a Linked Design layer (e.g. an illustration pulled from another author). The existing async resolver path handles it; no special book code needed. Worth a smoke test in Phase B since this is the first place we render a Linked Design inside a page that is itself fetched via the resolver — a two-level lookup.
  • Layer types known to work on a book page. All existing layer types (shape, image, text, qr, pixelart, design, slide) render unchanged on book-page / book-master / book-cover-* targets — the renderer keys off layer type, not target. The new text-frame layer is the only book-specific layer type.

Acceptance summary

Phase A — Book mode skeleton

  • templateMode: 'book' saves + restores via autosave.
  • 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 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

  • Add / duplicate / delete / drag-reorder writes back to 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 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.
  • Master-layer underlay is non-interactive in the page editor (hover shows an "M" badge, click passes through).
  • A master can carry a text-frame layer; the editor renders it as a labelled rectangle but the frame is empty (no reflow yet).

Phase C.5 — Reflowable text

  • A reflow entry on the manifest renders the chapter's markdown into the chosen master's text-frame, paginating across as many pages as the prose needs.
  • Each markdown element type (H1 / H2 / H3 / body / blockquote / code / list / link / image / hr) renders with the typography defined by spec.textStyles.
  • keepWithNext on headings prevents a heading-as-last-line on any page; widow / orphan rules prevent single-line splits.
  • {chapter-title} / {chapter-number} tokens on master text layers resolve to the containing chapter's values.
  • Designed pages with anchor.mode: 'before-chapter' | 'after-chapter' interleave around reflow segments correctly, and stay in place semantically when prose changes.
  • Pin / Live revision tracking: a pinned chapter ignores upstream updates; a live chapter surfaces an update notice with page-count delta.
  • "Preview pagination" shows page count + overflow warnings before publish.

Phase D1 — Per-event publish

  • Saving a page publishes a casewrap-page kind-30078 event under the user's pubkey.
  • Saving a chapter publishes a kind-30023 NIP-23 article event; the same event opens cleanly on Habla.news as a long-form article.
  • Reopening a book on a second device resolves every page / master / cover / chapter 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.

Phase E — PDF export

  • Export modal produces a PDF with the expected page count + cover spread on a separate page.
  • Reflowed prose exports as real PDF text (searchable + copyable) for fonts on the allowlist; a per-export warning lists any layers that fell back to vector paths.
  • 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.
  • "Proof one page" / "Proof one chapter" produces a single-page PDF in under 2 seconds end-to-end.

Phase F — Templates + import polish

  • At least three book templates ship at common sizes (US Trade 6×9, A5, US Letter); each opens as a fresh book with master(s) configured.
  • Import-from-deck produces a book of N designed pages whose layouts match the deck's slides (letterboxed when the deck aspect doesn't match the book trim).
  • Import-single-page from another book lands a Linked-Designs ref in the manifest without re-publishing the page.

Phase G — Reader Mode + embed

  • Reader Mode walks the manifest's pages with progressive fetch; prev / next page navigation works keyboard + touch + click.
  • Reader Mode works on a book with no reflow segments (designed-only) and on a book with only reflow segments (prose-only).
  • 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.
  • A book whose manifest itself is gated shows only its cover thumbnail + price to unauthorised readers; the page list resolves after unlock.

Phase I — Markdown editor + extended subset

  • CodeMirror editor replaces the plain textarea for reflow entries; syntax highlighting + smart list continuation + drag- and-drop image insert work.
  • Each extended markdown feature (tables, footnotes, nested lists, definition lists) has typography rules in spec.textStyles, paginates correctly across page breaks, and is acceptance-tested with its own fixture set in the engine's canonical corpus.
  • {toc} magic token expands into a styled list of chapter titles with page numbers; updates live as reflow re-paginates.
  • Cross-references ([See chapter 3](#chapter-3)) resolve to the right page number in both Reader Mode and PDF output.
  • PDF export includes a bookmarks outline built from chapterTitle per reflow segment.