From 79c89101c4d1d90978a4aa85f11712fee02365b0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:36:45 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20Phase=20J=20spec=20=E2=80=94=20Linked?= =?UTF-8?q?=20Content=20+=20threaded=20text=20frames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drafts the next architectural arc for the Book Designer around two ideas: - Events are linked files. Generalizes the pin-vs-live toggle shipped for chapters to every external reference. A new book.links[] collection becomes the project's 'Links panel' (chapters, masters, pages, illustration sets, future asset kinds). - Threaded text frames (InDesign-style). First-class text-frame layers on designed pages, threaded port-to-port; a thread declares a content source (a linked chapter event) and the pagination engine pours text through the frame chain. The current 'reflow entries in book.pages' model collapses into links[] + threads[] + auto-generated designed pages — a one-time migration on load. Pagination engine, PDF export, Reader Mode, Linked Designs resolver, and NIP-23 chapter events all survive. Phasing J1..J6 captured in the spec + TODO. No code yet — this is just the design doc and arc placement. Co-Authored-By: Claude Opus 4.7 --- TODO.md | 34 +++ docs/BOOK_LINKED_CONTENT_FEATURE.md | 422 ++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 docs/BOOK_LINKED_CONTENT_FEATURE.md diff --git a/TODO.md b/TODO.md index 566a5ca..5b9e379 100644 --- a/TODO.md +++ b/TODO.md @@ -63,6 +63,40 @@ Big arc. v1 supports a tight markdown subset for prose; explicitly deferred to Phase I: tables, footnotes, nested lists, embedded HTML, multi-column text frames, float-around-image. +### Phase J — Linked Content + threaded text frames + +Spec at `docs/BOOK_LINKED_CONTENT_FEATURE.md`. Reframes the Book +Designer around two ideas: + +- **Events are linked files.** Generalizes the pin-vs-live toggle + shipped for chapters to every external reference — masters, + designed pages, illustration sets, future asset kinds. A new + `book.links[]` collection is the project's "Links panel." +- **Threaded text frames** (InDesign-style). First-class + `text-frame` layers on designed pages, threaded port-to-port; a + thread declares its content source (a linked chapter event) and + the pagination engine pours text through the frame chain. + +Phasing: + +- **J1** Linked Content tab + Pages overview filters to designed + pages only; one-time migration of legacy `type: 'reflow'` entries + into `links[]` + `threads[]` + auto-generated designed pages. +- **J2** First-class `text-frame` layer on `book-page:` targets + (today they're master-only). +- **J3** Threading UI — in / out ports, load-cursor, break / show + thread. +- **J4** Thread-aware pagination engine — walk `thread.frames[]` + instead of cloning a master N times. +- **J5** Thread ↔ chapter assignment in the Linked Content library + (relink, embed, show usages). +- **J6** Polish — autoflow on Shift-click, overset warnings, cycle + detection, bulk relink / embed-all. + +The pagination engine, PDF export, Reader Mode, Linked Designs +resolver, and NIP-23 chapter events all survive — this is a UI + +data-model refactor, not a rebuild. Manifest schema version bumps. + ## Design-tool follow-ups (Illustrator parity arc) The pen / select / pathfinder bundle (`docs/PEN_TOOL_FEATURE.md`) diff --git a/docs/BOOK_LINKED_CONTENT_FEATURE.md b/docs/BOOK_LINKED_CONTENT_FEATURE.md new file mode 100644 index 0000000..60f5b62 --- /dev/null +++ b/docs/BOOK_LINKED_CONTENT_FEATURE.md @@ -0,0 +1,422 @@ +# Book Designer — Linked Content + Threaded Text Frames + +Status: **Drafted, not yet implemented.** This document describes the +next architectural arc for the Book Designer (the "Phase J" follow-on +to `BOOK_DESIGNER_FEATURE.md`). The current implementation works but +mixes content and layout in ways that don't scale to design-driven +typesetting; this spec lays out the upgrade. + +## TL;DR + +1. **Events are linked files.** A book references Nostr events by + coordinate the same way an InDesign project references images, + text files, and master spreads — by link, not by embedded copy. + This generalizes the pin-vs-live toggle already shipped for + chapters to every external asset the book references. +2. **Linked Content library** replaces the current "chapters mixed + into the Pages overview" model. A dedicated tab lists every + external event the book links: chapter articles, shared masters, + designed pages from other books, illustration sets. +3. **Threaded text frames** (InDesign-style) replace today's + "one master-text-frame paginates into N copies." Designed pages + carry first-class `text-frame` layers with stable ids; the author + threads frames port-to-port across pages; a thread declares its + content source (a linked chapter event) and the pagination engine + pours text through the chain. +4. The Pages overview filters to designed pages only — no more 250 + thumbnails for a 5-chapter book. + +## Vision + +The Book Designer should feel like InDesign with a Nostr-native +distribution model. Authors design pages bespoke (cover, frontmatter, +chapter-start spreads, full-bleed illustrations) and *link* content +(chapter text, shared masters, designed pages, illustration sets) by +referencing Nostr events. Text flows through user-arranged frames the +same way it flows through linked frames in InDesign. The book manifest +stays small — references, not payloads — and updates to any linked +event propagate to every book that links it. + +## Mental model — events as linked files + +| Nostr event kind | InDesign equivalent | Already shipped | +|---|---|---| +| `kind:30023` NIP-23 article (chapter markdown) | `.txt` / `.md` placed file | ✅ chapter import + pin/live toggle | +| `kind:30078` casewrap-page, `role:'cover-spread'` | Cover-spread master library | ✅ | +| `kind:30078` casewrap-page, `role:'master'` | Master page | ✅ | +| `kind:30078` casewrap-page, `role:'interior'` | Designed page from another book | ✅ Phase F3 import | +| Linked design layer (any flavor) | Placed `.psd` / `.ai` | ✅ Linked Designs spec | +| `kind:30078` casewrap-book manifest | The `.indd` project file | ✅ | + +The pattern is consistent. What's missing is treating all of these as +first-class entries in a **Links panel** the way pro tools do — with +status (live / pinned / encrypted / broken), update notifications, and +relink / embed actions. + +## Current vs proposed data model + +### Today + +``` +book { + spec: { trim, bleed, spineFromPages, ... } + cover: { ref } + masters: [{ ref, label }, ...] + pages: [ + { type: 'design', id, ref, masterId, anchor, gating, ... }, + { type: 'reflow', id, chapterMarkdown, chapterTitle, ref, pinnedEventId, masterId, ... }, + ... + ] +} +``` + +Reflow entries carry inline `chapterMarkdown` (when locally edited) or +a `ref` to a NIP-23 article event. Mixed into the same ordered list as +designed pages. The pagination engine clones the entry's `masterId` +into N physical pages and pours markdown into the master's first +`text-frame` layer. + +### Proposed + +``` +book { + spec: { ... } + cover: { ref } + masters: [{ ref, label }, ...] + pages: [ // designed pages only + { id, ref, masterId, anchor, gating, ... }, + ... + ] + links: [ // the "Links panel" — every external reference + { + id, // local link id (stable across renames) + kind: 'chapter' | 'master' | 'page' | 'design' | 'image', + ref: { kind, pubkey, dTag, relays, encrypted }, + mode: 'live' | 'pinned', // generalizes the chapter pin/live toggle + pinnedEventId: string | null, + label: string, // user-friendly display name + lastResolvedAt: number, // for "X minutes ago" hints + lastEventId: string, // for "update available" detection + }, + ... + ] + threads: [ // text flows + { + id, + sourceLinkId, // points to a links[] entry of kind 'chapter' + frames: [ // ordered chain of frame refs + { pageId, frameId }, + ... + ], + overflow: 'truncate' | 'autoflow', // policy when text exceeds frames + }, + ... + ] +} +``` + +Reflow entries disappear from `book.pages`. The chapter content lives +in a `links[]` entry; the flow path lives in a `threads[]` entry; each +designed page can host any number of `text-frame` layers. + +### Migration + +Existing books need a one-time migration on load: + +1. For each `pages[]` entry where `type === 'reflow'`: + - Create a `links[]` entry of kind `'chapter'` (carrying the entry's + `ref` / `pinnedEventId` / `chapterTitle`). + - Auto-generate N designed pages (one per paginated physical page) + using the entry's `masterId`. Each new page gets one `text-frame` + layer matching the master's text-frame geometry. + - Create a `threads[]` entry that chains those N frames and points + at the new `links[]` entry. +2. For each remaining `pages[]` entry (`type === 'design'`), drop the + `type` discriminator — every entry is now a designed page. + +Migration runs once at load when an old-schema book is detected (by +the presence of any `type: 'reflow'` entry). The original +`chapterMarkdown` field is preserved on the link entry so locally- +edited (not-yet-published) chapters survive. + +## Threaded text frames + +### Frame layer + +A new layer type (or an upgrade of the existing `text-frame` master +layer) with this shape: + +``` +{ + id, + type: 'text-frame', + target: 'book-page:', + x, y, w, h, rotate, opacity, z, + frameId, // stable id, unique within the page + threadId: string | null, // which thread this frame belongs to + // No content of its own — content flows in from the thread. +} +``` + +Stable `frameId` survives layer reorders so threads don't break when +the user fiddles with z-order. The wrapper `id` is the layer's +identity in `state.layers`; `frameId` is the thread-addressable id. + +### Threading UI + +In InDesign every frame has two ports: + +- **In port** — small square top-left. Visible state: + - Empty: nothing flowing in (head of a thread or unlinked frame). + - Arrow ▶: text is flowing in from a previous frame. +- **Out port** — small square bottom-right. Visible state: + - Empty: end of thread, no overflow. + - ▶: linked to another frame; mouse over to see which. + - Red ⊞: overset — text wants more frames but the chain ends. + +Linking: + +1. User clicks the out port of frame A → cursor changes to a "loaded + text cursor" carrying that link. +2. User clicks the in port of frame B → A's `nextFrame` becomes B; B + joins the same thread. +3. Esc cancels the load. + +Hover over either port to see the thread (highlight every frame in +the chain). Right-click for "Break thread" (split the chain at this +frame) or "Remove from thread" (yank this frame out, re-link its +predecessor to its successor). + +### Pagination engine changes + +Today: clone master N times, fill each master's first text-frame. + +New: for each thread, walk `frames[]` in order: +1. Resolve the source link's markdown (live event or pinned snapshot). +2. Run the existing pagination engine, but instead of producing pages, + produce **frame fills** — each frame in the chain gets one + page-worth of blocks (with layout for that frame's geometry). +3. When text runs out, leave remaining frames empty. +4. When frames run out with text remaining, mark the last frame as + overset (red ⊞ + warning hint). + +The existing pagination engine (greedy line breaking, block model, +widow/orphan rules) doesn't change — only the "what's the frame +geometry" and "what's the page boundary" inputs do. + +### Multiple threads per book + +A book may have many threads: + +- Main prose thread (Chapter 1 → 2 → 3, flowing through Chapters 1-50 + of designed pages). +- Sidebar thread (recurring callout text in a sidebar frame). +- Front-matter thread (the colophon + acknowledgements). + +Each thread is independent. A frame belongs to at most one thread. + +### Auto-flow + +When the loaded text cursor (carrying a chapter waiting to be placed) +is Shift-clicked on an empty area, generate enough new designed pages +to fit the remaining text, using the active master as the page +template. This is InDesign's autoflow. UX: + +- Place chapter on frame A: text fills A, overflow indicator appears. +- User Shift-clicks the out port: engine generates new pages with the + master template's text frames, threads them all to A, fills. + +Without autoflow you can still drag-create new frames manually and +thread them — autoflow is the convenience layer. + +## Linked Content library tab + +A new top-level tab in book mode (alongside Pages and Cover). UI: + +``` +┌─ Linked Content ─────────────────────────────────┐ +│ [+ Add chapter from Nostr] [+ From JSON] [↻] │ +├──────────────────────────────────────────────────┤ +│ 📄 Chapter — "Introduction" live · ✏ │ +│ naddr1... · author: Adam · 1.2 KB · 12m ago │ +│ │ +│ 📄 Chapter — "Body" [encrypted] pinned · ✏ │ +│ naddr1... · author: Adam · 8.4 KB · 1h ago │ +│ │ +│ 🎨 Master — "Standard interior" live · ✏ │ +│ naddr1... · author: Adam · 0.4 KB · just now│ +│ │ +│ ⚠ Chapter — "Footnotes" [unresolved] live · ↻ │ +│ Couldn't fetch from your relays. │ +└──────────────────────────────────────────────────┘ +``` + +Per-entry actions: + +- **Edit** — for chapters: open the markdown editor pane. For masters + / pages: open the surface in the canvas (existing flows). +- **Toggle pin / live** — same widget as today's chapter pin toggle. +- **Relink** — replace this entry's `ref` with a different event + coordinate (paste a new naddr). +- **Embed** — copy the resolved payload inline into the manifest so + the book is offline-safe. Loses the auto-update behavior. Reversible + by adding back as a link. +- **Show usages** — highlight every thread / layer that references + this link. +- **Remove** — only allowed when no thread or layer uses this link. + +Status badges: + +- 🟢 `live` — resolves to the latest event on each load. +- 🟡 `pinned` — locked to a specific `eventId`, ignores republishes. +- 🔒 `encrypted` — private (NIP-44 / NIP-04 encrypted to the author's + pubkey). +- ⚠ `unresolved` — last fetch failed. +- ⬆ `update available` — pinned, but the live version has changed. + +## Phasing + +### Phase J1 — Linked Content tab + Pages filter (the foundation) + +**Scope.** Pure UI / data refactor; no new rendering capability. + +- Add a new tab in book mode: "Linked Content" or "📎 Links." +- Move the "Add chapter" affordances out of the Pages overview. +- Filter the Pages overview to designed pages only. +- Migration: convert legacy `book.pages` entries with `type: 'reflow'` + into `book.links[]` + `book.threads[]` entries with auto-generated + designed pages (one per paginated page, each holding a single + text-frame matching the master's frame geometry). +- The pagination engine, PDF export, and Reader Mode continue to work + by reading from `threads[]` instead of `pages[].type === 'reflow'`. + +**What survives unchanged.** PDF export pipeline, NIP-23 chapter +event format, the Linked Designs resolver, the disk cache, the +manifest publish flow (apart from the new collections). + +**What changes.** `book.pages` becomes homogeneous. `book.links` and +`book.threads` collections appear in the manifest payload (schema +version bumps). The Pages overview drops its reflow-tile rendering +branch. + +### Phase J2 — First-class `text-frame` layer on designed pages + +- Promote the existing master-only `text-frame` layer to work on any + `book-page:` target. +- Stable `frameId` field independent of layer id. +- Sidebar tool: "+ Text frame" alongside the existing Pen / Shape / + Text tools, only enabled in book-page edit mode. +- For now: a placed frame is unlinked (no thread). +- Render unlinked frames as a thin dashed outline + the placeholder + text "Empty frame — link to a thread to fill." + +### Phase J3 — Threading UI + +- Render in / out ports on every `text-frame` layer. +- Implement the load-cursor flow: click out port → cursor loads → + click in port to link. +- Visual indicators: arrows on linked ports, red ⊞ on overset. +- Right-click menu: Break thread / Remove from thread / Show thread. +- Show-thread overlay: dim everything except the thread's frames. + +### Phase J4 — Thread-aware pagination + +- Refactor the engine: walk `thread.frames[]` instead of cloning the + master N times. +- The thread's `sourceLinkId` supplies the markdown (resolved from + `book.links[id]` per its mode/pinnedEventId). +- Frame-by-frame pagination — each frame gets one chunk of the layout + based on its own width / height (frames in a thread can have + different sizes; the engine recalculates line breaks per frame). +- Update the per-tile thumbnail painter and the PDF export to follow + the same thread walk. +- Backwards compat: a master that has one text-frame is still useful + as a template — autoflow uses it. + +### Phase J5 — Thread ↔ chapter assignment + Linked Content library + +- Wire the Linked Content tab to the new data model. +- Per-link actions: edit / pin-live toggle / relink / embed / remove. +- Loaded-cursor source: "Place chapter into frame…" creates a new + thread or joins an existing one. + +### Phase J6 — Polish + +- Autoflow (Shift-click loaded cursor → generate pages from master). +- Overset warnings + a small "X frames overflow" badge on the Pages + overview. +- "Show overflow text" inspector for an overset thread. +- Bulk relink / embed all / update-available chips. +- Cycle detection (frame A → B → A is rejected at link time). + +## What's deferred / out of scope + +- **Multi-column text frames.** Single-column only in v1. Phase K. +- **Wrap-around-image text wrapping.** Phase K. +- **First-line drop caps as a thread-level style.** Phase K. +- **Floating frames anchored to text positions.** Phase L. +- **Master frames** (frames that exist on a master and replicate to + every page using that master, each with its own thread membership) + — needs a separate spec. +- **Variable-data publishing** (mail-merge from a Nostr list event) + — interesting but separate arc. + +## Open questions + +1. **Frame ownership inside masters.** If a master defines a frame and + a page using that master places a different frame at the same + geometry, who wins? Proposal: master-frames are visible-only on + the page (not editable); the page can ignore the master entirely + by adding its own text-frame layer. +2. **Thread persistence on master swap.** If a thread runs through 10 + pages and the user changes one page's master, does the thread + break? Proposal: no — threads track frames, not masters. Changing + a master may move the frame visually but the thread holds. +3. **Encrypted chapter resolution latency.** Decryption happens on + resolve, which is async. The Linked Content tab should show a + spinner state ("decrypting…") and gracefully degrade if the + extension lacks NIP-44. +4. **Embed-all size cap.** Embedding every linked event into the + manifest can push it past relay size limits. Need a guardrail and + a clear "manifest too large — keep some links?" prompt. +5. **Update notifications.** Live links should pull periodically (or + on demand via the existing ↻ Refresh button). Per-tab polling + feels wrong; better to check on book open + an explicit refresh. + +## How this interacts with shipped features + +- **Linked Designs** (`docs/LINKED_DESIGNS_FEATURE.md`) — designed + page layers that already use the same pin-vs-live pattern. The + new `links[]` panel surfaces them alongside chapters; nothing in + Linked Designs has to change. +- **Premium pages** (Phase H1) — gating moves to the page event + itself (already addressable), not the thread. A thread can span + free + premium pages; the locked panes show the premium-unlock UI. +- **Embed mode** (`docs/EXTERNAL_EMBED_FEATURE.md`) and **Reader + Mode** (Phase G) — both consume the resolved physical-page + sequence the manifest produces. They don't care whether content + came from a thread or a reflow entry; pre-migration they walk + `pages[]` reflow expansions, post-migration they walk + `threads[]`-driven layouts. The resolver handles the seam. +- **PDF export** — same. +- **Casewrap-book manifest schema** — bumps. Old manifests parse + cleanly (reflow entries pass through the migrator); new ones + require a reader that understands `links[]` and `threads[]`. + Forward compat: a v1 reader seeing a v2 manifest should warn + ("This book uses a newer schema") rather than crash. + +## Acceptance criteria for Phase J1 (the first ship) + +1. A book opened with legacy reflow entries auto-migrates to the new + shape without data loss; the rendered output (PDF, Reader Mode, + thumbnails) is pixel-identical before and after migration. +2. The Pages overview shows only designed pages; reflow entries no + longer appear there. +3. The Linked Content tab exists, lists every chapter the book + references, supports the existing pin-vs-live toggle, lets the + user open the chapter editor. +4. The Cover view, Document settings, Masters modal, PDF export, + Reader Mode, and the existing publish flows all still work + unchanged. +5. Existing tests still pass; one new test verifies the migration is + round-trip-stable (save → reload → identical render).