Files
Artstr-Studio/docs/BOOK_DESIGNER_FEATURE.md
T
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

1462 lines
69 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```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
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:
```js
// 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`)
```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-*
},
}
```
#### 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.
```js
{
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's `spec.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:
```js
{ 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.