Adds a 📖 Docs link to the sidebar footer (next to GitHub + artstr.org)
that opens a Read-the-Docs-style two-pane modal: topic sidebar on the
left, rendered markdown on the right.
Content (src/help/*.md, 11 files):
- index.md — overview + nav
- template.md — Template tab explainer
- designer.md — Designer tab explainer
- cover.md — Case cover wrap
- disc.md — Disc-label sheet
- jewel.md — Jewel-case insert
- customart.md — Custom art
- slide.md — Slide
- deck.md — Slide deck
- pixelart.md — Pixel art
- book.md — Book Designer (links out to the deeper
BOOK_DESIGNER_TUTORIAL.md + spec on GitHub)
Implementation:
- _DOCS_TOPICS registry maps slug → { title, group, file }. Sidebar
groups headings under "Start here", "App tabs", "Template-mode
layouts".
- loadDocsTopic(slug) fetches `help/<slug>.md` (force-cache), renders
via the already-vendored marked, caches the rendered HTML for
session-instant re-visits, and updates the URL hash via
history.replaceState so deep links work.
- #/help/<slug> hash route auto-opens the modal at that topic on
page load + on hashchange.
- Esc closes; backdrop click closes; internal `#/help/foo` links in
page content load via the dispatcher (no page bounce); external
http(s) links get target="_blank" + noopener.
Styling:
- Inherits the rest of the modal aesthetic (var(--line),
var(--accent), system font for chrome, Georgia serif for body
text). Dark-mode-aware backdrops on code / blockquote / table
blocks.
- Two-pane layout collapses to a narrower sidebar on <720px viewports.
Files live in src/help/ (not docs/) because Vercel's outputDirectory
is "src" — the production site only serves files under src/. The
canonical deeper docs (BOOK_DESIGNER_TUTORIAL, FEATURE specs) stay
in docs/ and are linked-out from the in-app pages.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Inline \`[^N]\` markers render as Unicode-superscript numerals
(⁰¹²³…) and \`[^N]: definition\` lines collect into a "Footnotes"
section appended to the chapter (after the body, separated by a
horizontal rule).
How it works:
- _processBookFootnotes(md):
1. Sweeps definitions via /^[ \t]*\[\^id\]:[ \t]+(.+)$/gm,
stores them in a Map keyed by raw id, strips the line from
the body.
2. Renumbers: each defined id gets a 1..N number based on first
encounter of its \`[^id]\` ref in the body. Refs without a
matching def pass through unchanged.
3. Replaces every resolved ref with _bookSuperscript(num) so
¹² renders as actual superscript glyphs.
4. Appends a "### Footnotes" section with one bullet per
definition in canonical (first-ref) order.
Substitution sites:
- paginateChapter: runs after the {index} expansion + marker
stripping, before xref resolution. Only fires when \`[\^\` is
present in the source so most chapters pay zero cost.
- markdownToSafeHtml: same flow for the live preview pane.
Known v1 limitation (deferred to I4.2):
- Definitions render as a single end-of-chapter section rather than
per-page in a footnote-frame layer on the master. Per-page
footnote placement requires:
* A new footnote-frame layer kind on masters (parallel to
text-frame).
* Page-aware definition collection in the pagination engine.
* Page-break logic that keeps a ref + its def on the same page.
Out of scope for this slice; the chapter-end section is the
pragmatic v1.
Phase I scope: I1 ✅ I2.1 ✅ I2.2 ✅ I3 ✅ I4 ✅ I5 ✅ I6 ✅.
All spec'd Phase I features ship in v1 form.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Authors mark indexable terms with \`^[term]\` inline anywhere in
chapter markdown. A standalone \`{index}\` line expands at render
time into a sorted alphabetical index with the page numbers the
term appears on.
How it works:
- _computeBookIndexEntries(book) walks every reflow chapter and
scans its markdown for the \`^[term]\` pattern. Each unique term
collects the page numbers (chapter-start granularity in v1) of
the chapters it appears in. Results sort alphabetically by
term.localeCompare. Chapter physical page counts are
re-derived via paginateChapter({ _skipToc:true, _skipXref:true })
so the index numbering tracks the same physical-page sequence
enumerateBookPhysicalPages produces.
- _formatBookIndexMarkdown(entries) emits a "## Index" heading
+ one bullet per term: "- term — Page A, Page B".
- _stripBookIndexMarkers(md) replaces every \`^[term]\` with just
the term text so the marker stays invisible in the rendered
body. Done before xref substitution so a marker inside a link
text doesn't confuse the matcher.
Substitution sites:
- paginateChapter: when opts._skipIndex is unset, expands
\`{index}\` lines + strips markers. When _skipIndex is true (the
inner pass triggered by the index builder), strips both the
\`{index}\` placeholder line AND the markers.
- markdownToSafeHtml: same expansion for the chapter editor
preview pane, so authors see the index as they type around it.
Phase I scope now: I1 ✅ I2.1 ✅ I2.2 ✅ I3 ✅ I5 ✅ I6 ✅. Only
I4 (footnotes with per-page footnote-frame layer on masters)
remains — it's the deepest of the bunch, requires a new layer kind
+ page-aware definition collection, and is the natural last slice
before the branch lands.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Markdown links pointing at in-book anchors get their link text
augmented with the destination's page number at pagination time.
Slug matching:
- _bookSlugify(s) = lowercase + non-alphanumeric → hyphen, trim
edge dashes (matches GitHub-style heading-anchor convention).
- _computeBookSlugToPageMap(book) walks every reflow chapter with
_skipXref:true to avoid recursion, building a slug → page-number
map that registers:
* Each chapter's slugified chapterTitle.
* "chapter-N" aliases for entries with chapterNumber.
* Each H1-H6 heading's slugified text (first occurrence wins).
Substitution sites:
- paginateChapter: when the chapter markdown contains `](#anchor)`
and !opts._skipXref, rewrites every matching link to include
" (p. N)" in its visible text. Renders through DOM tile thumbs,
Reader Mode, and PDF real-text emission the same way.
- markdownToSafeHtml: live preview pane gets the same rewrite so
the author sees page numbers as they type.
- Unknown slugs pass through unchanged so a typo'd anchor stays
visible as a regular markdown link instead of silently breaking.
Phase I now has every spec'd feature except I4 (footnotes) and I5
(auto-index from inline ^[term] markers) shipped end-to-end.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
GFM tables are now first-class content blocks across every reflow
rendering surface: tile thumbnails, Reader Mode, PDF export with
real text emission. Replaces the I2.1 "[Table — N×M]" placeholder.
Pagination engine:
- For \`token.type === 'table'\`, computes equal-width columns
(availW / colCount), wraps each cell's text into lines at the
column's content width, and tracks row heights = max line count
per row × line height + cell padding.
- Returns the block with \`tokenType: 'table'\` and a \`tableLayout\`
payload carrying { colCount, colWidthIn, cellPadIn, align,
headerCellLines, rows, header/row line-heights, totalHeightIn }.
- blockH baked from the layout so the page-break logic treats
tables identically to other blocks.
DOM (paintReflowPageOverlay):
- Table blocks render as a real <table> with table-layout:fixed
and per-column text-align, header in bold with a solid bottom
border, body rows separated by hairline borders. white-space:
pre-wrap preserves the engine's wrap output.
Canvas (renderBookReflowPhysicalPageToCanvas):
- Each cell draws via fillText at the column's offset; alignment
honours the cell's column align directive. Hairline strokes
separate the header row + each body row so the table reads as
a grid even at lowish DPIs.
PDF real-text emission:
- When the block's font resolves to a StandardFonts key, each
cell's lines emit as textOps with leftIn / widthIn / align
scoped to the column's content rectangle. Header cells force
the bold variant so the printer sees real bold glyphs.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
A standalone \`{toc}\` line in any chapter's markdown expands at
pagination time into a list of every reflow chapter's H1 / H2 / H3
headings paired with their physical page number.
Two-pass internally:
- _computeBookTocEntries(book) walks the manifest and paginates
each reflow chapter with { _skipToc: true } so the inner pass
treats {toc} as an empty placeholder, then collects every
heading block plus the page number it lands on. Driven off the
same numbering rules enumerateBookPhysicalPages uses
(startAt / firstNumberedPage / skipNumber / skipNumberOnFirstPage).
- paginateChapter, when called without _skipToc and the markdown
contains a standalone \`{toc}\` line, replaces that line with the
generated TOC markdown before lexing. The TOC chapter itself
paginates normally with the expanded content.
- markdownToSafeHtml does the same substitution so the chapter
editor preview pane shows the rendered TOC as the user types
around it.
Known v1 limitation:
- TOC page numbers come from the pre-expansion enumeration; if
the TOC itself takes N pages, downstream chapter starts shift
by ~N from what the TOC reports. For typical books this is a
1–2 page offset. A full converging iteration is a follow-up
(acceptable v1 trade-off so we don't spin paginating the same
book repeatedly on every render).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The X / duplicate / proof / master-picker chrome on Pages overview
tiles was rendering behind the composited master+page thumbnail
because text layers carry their own zIndex inline (layer.z) and
the actions container had no stacking context.
- .bookPageTileActions gains z-index: 20 and the per-button
children + master <select> get z-index: 21 with position:
relative so they create their own stacking contexts (necessary
for the select to dropdown above neighbouring tiles too).
- .bookPageTileNum gains z-index: 10 + a translucent white
background so the page-number chip stays legible over dark
text layers.
Reported: "the X button on the pages overview slide is behind
the master content. The pages buttons like the X should always
be rendered on top so the user can see them."
Co-Authored-By: Claude Opus 4.7 <[email protected]>
marked switches to gfm:true everywhere (lexer + parser), unlocking:
- Task lists — `- [ ] todo` / `- [x] done` parse as list items with
task:true + checked:bool. The pagination engine emits ☐ / ☑ as
the marker prefix (replacing the bullet) so paginated and preview
output both render the checkbox state.
- Nested lists — list items whose tokens contain an inner list
recurse with a 3-space-per-level indent. Capped at depth 6 so
pathological input can't blow the stack.
- Autolinks + strikethrough — parsed by marked into the HTML
preview pane (the pagination engine flattens to plain text so
strike doesn't visually render in paginated tiles, but the
inline preview shows it).
Tables get a placeholder row in v1 — "[Table — N cols × M rows: …]"
with a warning surfaced through pag.warnings. Full column layout
ships in I2.2 once the renderer side (DOM + canvas + PDF emission)
gains a per-cell branch.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Replaces the plain textarea with a CodeMirror 5 instance configured
for markdown — syntax highlighting, line wrapping, smart Enter for
list continuation.
Library:
- Vendored CodeMirror 5.65.16 as three files in src/vendor (core,
markdown mode, CSS). ~188 KB total. Lazy-loaded through
loadCodeMirrorOnce() so the cost is only paid when a chapter
opens.
Wiring:
- openBookChapter kicks off _upgradeChapterEditorToCodeMirror after
the existing sync runs. The upgrade swaps the textarea for a
CodeMirror instance bound to its DOM node and stores the editor
handle on ta._cmEditor.
- syncBookChapterEditor pushes value changes through the CM editor
when present (only if value differs AND the editor doesn't have
focus — preserves the user's caret + IME composition).
- Editor change → updateBookChapterMarkdown, which already debounces
history + repaints the markdown preview + page-count readout.
- Enter on a list line auto-continues the bullet (- / * / +) or
numbered marker (1. → 2.); pressing Enter on an empty list item
breaks out of the list.
- Falls back gracefully to the plain textarea if the CodeMirror
download fails (offline / blocked CDN re-host).
CSS:
- Forces .bookChapterEditorBody .CodeMirror to fill its grid cell
(CodeMirror's default is 300 px); matches the surrounding
monospace font + line-height so the pre/post-upgrade UX is
visually consistent.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Per-page editor gains a ⚡ Free / ⚡ N sats button that records
the user's paywall intent on the manifest entry. Pages overview
tiles surface a matching amber chip at the bottom-left so authors
can scan paywall coverage at a glance.
- openBookPageGatingDialog(entryId) prompts for a price in sats
(empty / "0" clears the gating). Records as
entry.gating = { mode: 'premium', priceSats, previewWatermark }.
- syncBookPageEditBar swaps the button's label, colour, and title
based on entry.gating; visible only when editing a page.
- renderBookPagesGrid paints a .bookPageTileGateChip on tiles with
premium gating.
Limitation (deferred): publishBookPageToNostr does not yet route
premium pages through _publishPremiumDesign — for v1 we persist the
intent on the manifest, and the page event still publishes via the
free path. Wiring the encrypted-publish + Reader-Mode unlock CTA
lands in a follow-up (H1.2 / H3).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The chromeless embed viewer now handles book manifests. The route
parser doesn't need a code change — any naddr that decodes to a
casewrap-book event falls into _paintEmbedBook automatically.
How it works:
- _paintEmbed dispatches mode === 'book' to a new _paintEmbedBook.
- The painter seeds state.book / state.layers / state.meta from
the parsed manifest, then awaits resolveBookSidecars() to pull
every referenced page / master / cover / chapter event from
relays. The disk cache from D1.6 makes warm embeds instant.
- After resolution, walks enumerateBookPhysicalPages and renders
the current page through the same canvas helpers Reader Mode
uses (renderBookDesignedPageToCanvas /
renderBookReflowPhysicalPageToCanvas) at ~144 DPI.
- One-page-at-a-time stage with click-to-flip (left half back,
right half forward), keyboard nav (← / → / Space / Home / End /
f for fullscreen), and prefetch of the next page.
- Footer chips honour the existing embed params:
?controls=0 hides prev/next/fullscreen
?attr=0 hides the author chip
?open=0 hides the "Open in Artstr ↗" link
?page=N (1-based) deep-links to a starting page
- Encrypted-private book sidecars resolve via the existing
_decryptBookSidecarEvent path, so the same key-wrap-to-self
rules apply (the embed can show private books for the signed-in
author across devices; foreign-author private events render
blank text as expected).
Phase G complete: Reader Mode in the editor + cut-down flip-book
in the embed iframe.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Full-screen reader launched from the Pages overview. Walks
enumerateBookPhysicalPages and renders each via the existing
export-canvas helpers (the same path the PDF pipeline uses) at
~144 DPI; pages cache in a session Map for instant revisits.
Affordances:
- Header: ☰ TOC toggle, book title, ✕ Close.
- TOC sidebar: chapter headings + designed-page labels with
current-page highlight; click jumps. Collapsible (closed by
default; auto-closes on mobile-width jumps).
- Stage: current page centered in the dark viewport. Click the
left half to go back, right half to advance. Hover-revealed
arrows on each side as a backup affordance.
- Footer: "N / total" counter, progress bar, keyboard-hint line.
Keyboard nav: → / Space / PageDown advance; ← / PageUp back;
Home / End jump to first / last; Esc exits. Handler is registered
on open and removed on close so the editor's shortcuts stay live
behind the modal.
Prefetch: after a page renders, the next two pages render
in the background (best-effort, errors silenced) so forward
flips feel instant once the user gets going.
Premium / encrypted handling: the underlying renderers already
decrypt via _decryptBookSidecarEvent and substitute tokens via
bookTokenContext, so encrypted-private books and chapter-token
masters Just Work in Reader Mode too.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
New "+ Import page" button in the Pages overview toolbar takes a
naddr / nevent / note / hex pointing at a casewrap-page event and
adds it to the manifest as a design entry referencing that event.
- Resolves through the shared _resolveEmbedEvent helper (works for
both authors' own and third-party events; honours encrypted
envelopes via _decryptBookSidecarEvent for sidecars private path).
- Validates kind 30078 + warns when role isn't "interior" before
adding.
- De-duplicates: refuses to add a page whose (pubkey, dTag) is
already in the manifest.
- Seeds the disk cache with the resolved event and applies the
page's layers immediately so the new tile paints without a second
relay round-trip.
- Carries the event's `title` tag into entry.label as a sensible
default sorter label.
Phase F is feature-complete per the spec: starter templates,
deck → book conversion, single-page import.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
A new "→ Convert to book" action lives in two places:
- Deck-sorter toolbar (next to "+ Add slide") — for users already
in deck mode who want to flip their existing slides into a book.
- Book starter panel's "Or import from a deck" block — surfaces
automatically when the user enters book mode with no pages and a
non-empty deck.
How the conversion works:
- convertDeckToBook({ presetKey }) resets the book to a starter
spec (US Trade 6×9 by default; can pass any BOOK_SIZE_PRESETS key)
with a default master.
- Each slide becomes one designed-page entry. Slide layers
letterbox-fit into the trim:
scale = min(trimW / slideWIn, trimH / slideHIn)
layers reposition to offset + x*scale, offset + y*scale
layers' w/h scale linearly; fontSize and stroke.width scale too
- Slide background (when non-default) carries to entry.background.
- Slide.name carries to the entry's label.
- Confirms before clobbering an existing book.
- Switches templateMode to 'book' on success + toasts a
"Imported N slides as designed pages." message.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
When a book has no pages yet, the Pages overview now shows a
four-card starter gallery (US Trade 6×9, A5, A4, Square 8×8) instead
of the bare "No pages yet" placeholder. Click a card → fresh book
seeded with:
- The preset spec (trim + bookSize label).
- A default master named "Body pages" with a text-frame layer at a
0.75 in margin type area — so reflow chapters reflow correctly
out of the box.
- A starter chapter with friendly placeholder markdown that
exercises headings / paragraphs / lists / blockquotes so authors
see the default Georgia / Helvetica stylesheet at work.
- A blank "Title page" designed entry (skipNumber:true) so the
designed-page editor isn't a dead-end either.
Implementation:
- applyBookStarterTemplate(presetKey) resets the book state
(preserving state.meta + community), seeds the master + chapter +
title page, drops any leftover book-* layers from prior sessions,
and clears lastPublishedManifestSerial so the next publish mints a
fresh casewrap-book event.
- renderBookPagesGrid's empty branch paints a four-card grid; each
card is a button with preset metadata + a one-liner use case.
Click runs applyBookStarterTemplate.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
One-click PDF proof of a single page or single chapter — useful for
sending to a printer to test colour / paper / paper-weight before
committing to a full book run.
- proofBookEntryPdf(entryId, opts) finds the entry's range in the
physical-page sequence (one slot for designed pages, N slots for
reflow segments) and calls exportBookToPdf with includeCover:false
scoped to that range.
- Filename = <book-slug>-proof-<entry-suffix>.pdf where the suffix
is the chapter title (reflow) or the page label / "page-N"
(designed) slugified.
- Onboard surfaces:
* Each Pages-overview tile gets a 📄 action between Duplicate
and Delete; click runs the proof for that entry.
* The per-page edit bar gains a "📄 Proof PDF" button next to
Publish — proofs the currently-open page.
* The chapter editor gains the same button — proofs all
paginated pages of the open chapter.
- Status flows back through the same hint slot each surface
already uses for Publish status, including warning surfacing
(failed images, fallback fonts).
Phase E now feature-complete per the spec: vendored pdf-lib +
trim/bleed/crop-marks/range/split modal + real PDF text for
reflowed prose + proof-one-page.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Reflowed chapter prose now emits as real PDF text — selectable,
searchable, vector — when the block's font maps to one of the 14
built-in PDF standard fonts (Times Roman, Helvetica, Courier
families × four styles each). A 200-page novel that exported as
hundreds of MB of PNG pages in E1 now exports as a few MB with
copy-paste-able text and full font fidelity.
How it works:
- _resolveStandardFontKey(family, bold, italic) maps a CSS family
name onto a pdf-lib StandardFonts key. Serif families (Georgia /
Times / etc) → TimesRoman; sans-serif (Helvetica / Arial / etc)
→ Helvetica; monospace → Courier. Returns null for off-list
fonts so the caller knows to fall back.
- renderBookReflowPhysicalPageToCanvas grew two optional outputs:
textOps (array — collected positioned text ops) and
fontWarnings (Set — families that fell back to raster).
- Per block: if the resolved fontKey is non-null and we're in
PDF-export collect mode, push one op per line into textOps and
SKIP the canvas draw for that block. Off-list blocks still
rasterise as before, but their family is added to fontWarnings.
PDF emission:
- exportBookToPdf maintains a per-doc font cache so each
StandardFonts variant is embedded once across the whole book.
- After each reflow page is stamped, the textOps are emitted via
pdfPage.drawText with width-of-text alignment maths
(font.widthOfTextAtSize) for centre/right alignment and a
baseline = pageHpt − topPt − size×0.8 conversion from inches-
from-trim-top to pdf-lib's bottom-up grid.
- _pdfRgbFromColor parses #hex / rgb() into pdf-lib's rgb() so
textStyles colours carry through.
UX:
- The per-export warning summary now includes
"N font families are not on the embeddable allowlist… those
blocks fell back to vector raster: <list>" so authors know which
fonts to swap if they want fully-real-text output.
Known limits (deferred):
- Designed-page text layers still rasterise — they have richer
styling (inline marks, rotation) that doesn't map cleanly to
pdf-lib text in one slice. Coming in a follow-up.
- Bold/italic resolution is based on fontWeight ≥ 600 / fontStyle
=== 'italic'; partial weights round to 400/700.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Replaces the one-click "Export PDF…" with a dedicated modal that
exposes the options the pipeline now supports.
Options:
- Page range — All / 1-based from–to. Editing either input flips the
radio so the user doesn't have to click it first.
- Include cover spread (default = on when the book has a cover).
- Include bleed margin — pages export at trim + 2×bleed, with the
trim canvas centered inside; the surrounding strip is the page bg.
- Crop marks — _drawCropMarks emits L-shaped marks outside each
trim corner via pdf-lib's drawLine (printer guidance for cutting).
- Output: single PDF (interior + cover) or split (interior + cover
as two files).
- Filename pattern: <name>.pdf, or <name>-interior.pdf +
<name>-cover.pdf for split mode. Default = title-slug.
Pipeline:
- exportBookToPdf now accepts {dpi, includeCover, includeBleed,
cropMarks, range:{from,to}, split}. Range filters
enumerateBookPhysicalPages. Bleed-inclusive pages are sized
trimW+2×bleed and place the trim canvas at the bleed offset, so
artwork extends past the trim edge only when bleed-aware layers
on the page actually do — for v1 the strip stays the page bg.
- Cover spread honours includeBleed too: when off, the bleed-
inclusive canvas draws at a negative offset so only the trim
area lands on the (smaller) page. Cover crop marks position at
the spread's trim corners.
- Split output returns { split: true, interior, cover, warnings };
single output keeps the existing { blob, warnings } shape.
Modal lifecycle:
- openBookExportModal seeds totals + sensible defaults.
- runBookExportFromModal reads inputs, calls exportBookToPdf, and
triggers the download(s). Auto-closes on success after 800 ms;
surfaces warnings inline.
- Click-outside + ESC close behave like other modals.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
First slice of Phase E. The Pages overview now has an Export PDF…
button that walks the physical-page sequence, renders each page at
print DPI, and stamps them into a single PDF via pdf-lib.
Library:
- Vendored [email protected].1 as src/vendor/pdf-lib.min.js (~513 KB,
MIT). Lazy-loaded through loadPdfLibOnce() so the cost is only
paid when an author actually exports.
Pipeline:
- exportBookToPdf({ dpi, includeCover }) walks
enumerateBookPhysicalPages(book), rendering each slot at the
resolved DPI (default 200) onto a trim-sized canvas:
* Designed pages → master underlay + page layers via
drawLayerToCanvas, with token substitution applied to master
text layers (book.spec.textStyles tokens resolve per page).
* Reflow physical pages → master underlay (with text-frame
outline stripped) + the pagination engine's block output drawn
via fillText at the frame's coordinates.
* Cover → wide spread canvas, all book-cover-* / book-spine
layers drawn in place.
- Each canvas converts to PNG via canvas.toBlob → Uint8Array, then
pdf-lib's embedPng + addPage at trimW×trimH points (cover spread
is its own landscape page at the end).
UI:
- Export PDF… button in the Pages overview toolbar with a status
line that surfaces progress + warnings (failed image embeds,
per-page render errors).
- Filename = `<title-slug>.pdf`.
Known v1 limitations (handled in later E phases):
- Raster-only: every page is a PNG inside the PDF. Real PDF text
for embeddable-allowlist fonts comes in E3.
- No bleed-inclusive output / crop marks / page-range / two-file
modes yet (E2).
- Linked-design layers + premium-gated content not exercised.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Two pieces that close out Phase D2:
Dirty tracking
- isBookManifestDirty() compares the current bookManifestPayload
serial to state.book.lastPublishedManifestSerial; a missing
snapshot but any existing ref counts as dirty so the sidebar
prompts the first publish.
- markBookManifestClean() snapshots the serial on success; called
from both the public and private manifest publish paths in
publishCurrentTemplateToNostr.
- syncBookSidebarSummary renders a one-line hint —
"⚠ Manifest has unsaved changes" or "✓ Manifest is in sync" —
so authors see when sidecar publishes have outpaced the manifest
publish.
Feed-card preview
- buildFeedCard now has a `book` branch: a chapbook-shaped card
(cream-to-amber gradient, Georgia serif title, page + chapter
counts) so book manifests don't fall through to a blank "Preview
unavailable" tile in the community browser.
- MODE_LABELS gets 'book': 'Book' so the type chip + fallback label
read correctly.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
After the resolver paints from the warm cache, a background probe
queries relays for the latest event matching each naddr ref and
compares against the cached created_at. Refs with a newer upstream
version stage in _bookUpdates; tiles render an "↑ Update" chip,
and the cover-view hint surfaces a matching "Apply update" button.
Click applies the event (decrypting envelopes when private), writes
through to the disk cache, and clears the flag.
Implementation:
- checkBookSidecarUpdates(book) batches every live naddr ref
(cover / page / master / chapter; pinned eventId refs skipped,
pinned chapter refs also skipped) by kind:pubkey, runs one
queryRelays filter per group, and stages newer events in a
session-only Map.
- applyBookUpdate(ref) reuses the resolver's apply helpers +
envelope decryption so the application path is identical to a
fresh fetch.
- Probe fires from resolveBookSidecars(non-force) on a 600 ms
delay so the warm cache paints first; force-refresh from the
sidebar skips it (the relays were already hit).
- Tile chip styling lives next to .bookPageTileNum; cover hint
injects an inline button when hasBookUpdate(book.cover.ref).
- Every successful Publish (page / master / cover / chapter) calls
clearBookUpdate(ref) so a re-publish doesn't leave a stale
chip pointing at the version the user just superseded.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
A re-open of a published book now paints instantly: the resolver
checks localStorage before relays, and every successful network fetch
+ every just-published event is written back to the cache.
Cache shape:
- Single localStorage key casewrap:bookSidecar:v1 with
{ version: 1, entries: { <refKey>: { ev, fetchedAt } } }.
- refKey is the addressable coord (`<kind>:<pubkey>:<dTag>`) or
`id:<eventId>` for pinned imports.
- 2 MB total content-size cap; LRU eviction by fetchedAt ascending.
- Read bumps fetchedAt so hot entries stay resident.
- Quota errors clear the cache so a follow-up write can succeed.
Wiring:
- resolveBookSidecars: warm path checks _bookCacheRead first; only
hits relays on a miss. summary now returns
{ resolved, failed, cached } so the refresh button can surface
cache hits ("Resolved 12 (10 cached) · 0 failed").
- Force-refresh (the sidebar's ↻ Refresh button) skips the cache so
the user gets fresh upstream state, then writes the result back.
- Cache writes also fire when each Publish handler completes (page /
master / cover / chapter) and on chapter import / refresh — so a
freshly published or imported event is already hot for the next
reopen.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Cover view is no longer a static summary. Click "Edit cover" → enters
cover-edit mode with the canvas widened to the full wrap-around spread
(back + spine + front + bleed); layers land under book-cover-front by
default; the layer-target dropdown lists front / spine / back as
available panels (filtered by coverHasSpine / coverHasBack so a
front-only chapbook just shows one panel).
State + body-class:
- state.book.editingCover flag; body class editing-book-cover.
- CSS gate now reveals sheet/layers/tools whenever editing-book-cover
OR editing-book-page is set; #bookView hides while editing.
- openBookCover / closeBookCover lifecycle helpers; the shared
bookPageEditBack button dispatches to closeBookCover when in
cover-edit mode.
Canvas wiring:
- sheetW returns spread width when editingCover, honouring
coverHasBack / coverHasSpine.
- targetBounds for book-cover-back / book-spine / book-cover-front
computes x offsets from the same flags so the front panel starts
at the bleed edge when there's no back / no spine.
- isLayerForCurrentMode and currentDrawTarget pick up the cover
surface; templateLayerTargetsForMode lists the three panels for
the sidebar layer-target select.
- syncBookPageEditBar handles the cover surface: relabels Back /
Publish, hides the prev/next arrows + text-frame button.
Publish:
- bookCoverPayload assembles a role:'cover-spread' payload —
cover layers retain their book-cover-front/back/spine targets, the
coverSpread block snapshots spine width + has-back/has-spine so the
event renders standalone.
- publishBookCoverToNostr mints kind-30078 with d-tag
book-cover:<title-slug>, reuses the existing ref's d-tag when
re-publishing own cover (NIP-33 replace). Honours
state.visibility for private encrypted publish.
- publishBookSurface dispatches to the cover helper when
editingCover is set, so the same Publish button serves all three
surfaces.
UI:
- Cover view gains an Edit cover button (primary).
- Ref hint line reports published / unpublished / "no artwork yet"
state with the d-tag and 🔒 indicator on encrypted publishes.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
When a casewrap-book manifest loads, the resolver progressively
fetches every referenced page / master / cover / chapter event and
grafts its content into state so a fresh client can reconstruct the
book from refs alone.
- _collectBookSidecarJobs(book, { force }) walks the manifest and
returns one job per ref whose layers / markdown aren't already in
state. Idempotent — autosave restore with cached layers triggers
nothing.
- _refToDecoded turns {kind, pubkey, dTag} or {eventId} into the
shape _resolveEmbedEvent uses (already proven on the embed route).
- resolveBookSidecars(book, opts) fetches every job in parallel,
decrypts envelopes via _decryptBookSidecarEvent (works for private
publishes from D3), and dispatches to per-kind apply helpers:
_applyResolvedBookPage — rewrites target to book-page:<id>
_applyResolvedBookMaster — rewrites to book-master:<id>
_applyResolvedBookCover — keeps book-cover-front/back/spine
_applyResolvedBookChapter — drops markdown into entry.chapterMarkdown
Concurrent calls coalesce on a single in-flight promise so spamming
"Refresh" doesn't fan out.
- Public NIP-23 chapter events (kind-30023) fall through to the raw
ev.content markdown path; encrypted chapters route through the
decrypt helper.
Wiring:
- loadProjectFromText: new explicit `book` branch sets state.book,
preserves any existing book-* layers, drops non-book layers, then
kicks off the resolver async. Default textStyles + spec are merged
under the loaded ones so older manifests with missing keys still
render correctly.
- restoreAutosave: when the restored project is templateMode='book',
fires the resolver to backfill any sidecars whose layers aren't in
the cached state.
- New ↻ Refresh from Nostr button + status line in the book sidebar
invokes resolveBookSidecars(state.book, { force: true }) so the
user can manually pull fresh upstream changes.
This closes the round-trip: publish a manifest + sidecars → open
the manifest on another device → resolver reconstructs the book.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Wires chapter / page / master publish to honor state.visibility ===
'private' by routing through the existing PRIVATE_PUBLISHING flow,
plus a visibility chip in the book sidebar so authors see and can
flip the current mode without opening the publish modal.
What changed:
- _publishEncryptedBookSidecar(payload, dTag, extraTags) — wraps
encryptProjectEnvelope + publishCommunityEvent with an explicit
stable d-tag, so re-publishes replace via NIP-33 (the existing
publishEncryptedDesign uses state.privateDTag, which is wrong for
per-entry sidecars).
- _decryptBookSidecarEvent(ev) — symmetric helper; returns null for
non-envelope content so the public NIP-23 import path still works.
Per-helper changes:
- publishBookPageToNostr / publishBookMasterToNostr: branch on
state.visibility; encrypted path uses the same book-page:<id> /
book-master:<id> d-tag and flags entry.ref.encrypted = true.
- publishBookChapterToNostr: public path stays kind-30023 with
title-slug d-tag; private path switches to kind-30078 with an
encrypted envelope under book-chapter:<entry-id> (the slug would
leak title even though content is encrypted).
- importBookChapterFromNostr / refreshBookChapterFromNostr:
detect encrypted envelopes and decrypt before storing into
chapterMarkdown; surface a clear error when the user can't decrypt
(foreign author's private event).
UI:
- New chip in the book sidebar — "Public" / "🔒 Private" — clickable
toggle. Disabled when the extension lacks NIP-44/NIP-04. Note line
explains exactly what each mode does.
- Per-page / per-master / per-chapter Publish buttons gain a 🔒
badge + tooltip when in private mode, so authors see what's about
to happen without leaving the editor.
- Sidebar chip + Publish-button labels stay in sync via the existing
render() pass (syncBookSidebarSummary → syncBookVisibilityChip),
the publish-modal radio handlers, and a manual sync after the chip
toggle so the change is immediate.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Wires the existing top-bar Publish flow so a book in templateMode='book'
emits a casewrap-book manifest event (kind-30078) that references the
sidecar page / master / cover / chapter events instead of inlining
their layers.
- bookManifestPayload() returns a lean manifest — just the spec, the
cover/page/master refs, the per-entry typography overrides, and
chapter metadata. Layers stay on their per-surface events.
- _buildBookAttributionTags walks every referenced ref and emits
matching `p` (role: 'book-contributor') + `link` tags so external
clients can resolve the sidecars and credit the right authors.
- publishCurrentTemplateToNostr + the communityPublishBtn pre-flight
both gain a `book` branch that:
* refuses to publish empty books,
* refuses to publish when any designed page lacks a ref (would
point at nothing), telling the author which pages to publish
first.
- Publish confirm modal renders a lightweight book card (title +
trim size + entry counts) until cover-view ships a real composited
preview.
- MODE_LABELS_PRETTY gets a 'book' label so the modal type chip
reads "Book manifest".
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Adds a Publish button to the per-page / per-master editor bar that
mints a kind-30078 casewrap-page event for that single surface,
signs through window.nostr, publishes to user relays, and stores
the resulting naddr coord on the manifest entry / master.
- bookPagePayload(entry, role) builds the standalone payload —
layers filtered to just this entry, target prefix dropped so the
event renders cleanly through Linked Designs / embed routes.
- D-tag: `book-page:<entry-id>` or `book-master:<master-id>`, stable
across renames so re-publishes replace in place via NIP-33.
- publishBookSurface dispatches to page vs master publish based on
which mode is active.
- Bar relabels the button "Publish page" / "Re-publish page" /
"Publish master" / "Re-publish master" with the matching title
tooltip so authors see the d-tag they're about to overwrite.
- Status hint inline next to the button surfaces signing / publish
outcome.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
First chapter of Phase D — the chapter editor now has a Publish
button that mints a kind-30023 article from chapterTitle +
chapterMarkdown, signs through window.nostr, publishes to the user's
relays, and updates entry.textRef so subsequent reads come from
their own event.
- d-tag: reuses entry.textRef.dTag when the user is re-publishing
their own existing article (NIP-33 replaceability); otherwise mints
`chapter:<title-slug>` falling back to `chapter:<entry-id>`.
- Tags: d / title / published_at / t=book-chapter.
- Confirms before clobbering a foreign-author textRef with the
user's own publish.
- Own publishes default to live (textPinnedEventId = null) so future
edits to the same d-tag flow into the book automatically.
- Sourcebar status flips to "Live · d:…" with the user's own ref;
Refresh / Pin / Unlink continue to work against the new ref.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Adds a source bar to the chapter editor so authors can drop in a
naddr/nevent/note/hex id, pull the markdown via the shared relay
resolver, and choose whether the chapter follows future edits.
- Import button decodes the id with decodeNostrId, resolves the event
through _resolveEmbedEvent (same logic the embed route uses), copies
.content into chapterMarkdown, and records textRef.
- nevent imports pin to the resolved event id; naddr imports default
to live (follow d-tag).
- Pin/live toggle: pinning a live chapter resolves the current event
id and freezes it; flipping to live just clears textPinnedEventId.
- Refresh re-resolves the linked source — picks up new versions on
live links, just re-fetches the same id on pinned links.
- Unlink confirms then drops textRef / textPinnedEventId. The local
markdown stays put.
- Source-bar status chip: "Local only" / "Live · d:…" / "Pinned · …",
with short id + author preview. Disables the import input while
linked so the path is unambiguous.
- Picks up the article's NIP-23 title tag when the chapter is unnamed.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Each tile in the Pages overview now has a hover-revealed master picker
that overrides effectiveMasterIdForPage for that single entry.
- Empty selection ("Default (…)") falls through to spec.defaultMasterId.
- "No master" explicitly opts out via masterId === null.
- Otherwise persists the chosen master id on the manifest entry.
- Unified the reflow tile + pagination engine + physical-page enumerator
to call effectiveMasterIdForPage instead of the local
`p.masterId || defaultMasterId` short-circuit so the explicit-null
semantics work everywhere.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Surface book.spec.textStyles as an editable Typography section inside
the Document settings modal so authors can override the v1 markdown
stylesheet without dropping into JSON.
- Per-group schema lists the fields to surface (font/size/weight/color/
alignment/margins/etc). Adding a row in BOOK_TEXT_STYLE_FIELDS makes
the field render + persist automatically.
- renderBookTypographyEditor merges defaults under the live spec when
populating, so selects show the effective value rather than the empty
default — e.g. a default fontWeight of 700 doesn't render as 400 just
because the user hasn't customized it yet.
- Apply merges editor output over existing spec + defaults so partial
customizations don't wipe untouched fields.
- Reset button restores defaults in the editor without applying.
- After Apply, render() repaints reflow tiles and any open chapter
editor's readout refreshes — typography changes can move page breaks.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
When a master text layer renders on top of a specific page — in tile
thumbnails or the canvas underlay during book-page edit — `{page-number}`,
`{page-count}`, `{physical-page-count}`, `{book-title}`, `{book-author}`,
`{chapter-title}` and `{chapter-number}` resolve against that page's place
in the manifest.
- `enumerateBookPhysicalPages` walks the manifest into a flat physical-page
sequence, paginating reflow segments and threading chapter context.
- `bookTokenContext` returns the context for a given physical page within
a manifest entry (designed pages use indexInEntry=0).
- `substituteBookTokens` does HTML-safe string replacement; values are
escaped before insertion so a chapter title with `<` or `&` can't break
the layer markup.
- Tile thumbnails for both designed pages and reflow segments now run
master layers through `applyBookTokensToLayers` before composing.
- The canvas editor substitutes tokens on master text layers when they
appear as the page-edit underlay (and disables contenteditable so the
substituted text can't accidentally overwrite the master's source).
Master-edit mode leaves tokens literal.
- `bookPageCount` (spine-width input) now counts physical pages.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Reflow tiles in the Pages overview now show a real composited
preview: master layers underneath (text-frame outline removed —
the prose fills its place visually), then the first paginated
page of the chapter rendered as positioned text blocks on top.
- New `paintReflowPageOverlay(host, page, frame, trimW, trimH)`
helper: takes one paginated page + the master's text-frame
geometry and paints each block as an absolutely-positioned
div inside the thumb. Block styles (font, size, weight, leading,
alignment, colour, background) all come from the engine's
resolved style; coordinates convert to percent of the canvas
inch-space so the overlay tracks any thumb size.
- Tiles get a page-count suffix: "Chapter 1 · 14 pg".
- Master layers in reflow thumbs skip any text-frame layer (its
outline is meant for the editor, not the rendered output —
the prose IS the frame's content at render time).
Phase C.5.3 ships tile thumbnails; the chapter editor's right
pane stays as an HTML preview for now. A paginated-preview
toggle there lands in a later slice.
New pure function `paginateChapter(entry, book)`:
- Resolves the chapter's effective master + finds its sole
text-frame layer. No master / no frame → returns warnings,
empty pages, so the UI can hint at what's missing.
- Tokenises the markdown via `marked.lexer` (token shapes, not
HTML), then for each token builds a "block" carrying the
laid-out lines + the style metadata + the total height.
- Block kinds today: paragraph, h1-h6, blockquote, code (no
wrapping inside code blocks), list (flat "• item" / "1. item"
lines), hr. Inline runs (bold / italic / inline-code) collapse
to plain text for now — Phase I gives them per-run styles.
- Text wraps greedily into lines that fit the frame's available
width (frame.w minus 2 × frame padding minus left/right
margins from the block's style). Words too wide to fit are
emitted anyway and surfaced as a warning so the author can
spot them.
- Pages fill until adding the next block would exceed frame
height, then a new page starts. `keepWithNext` (true on h1-h6)
pushes a heading to the next page if its following block
wouldn't fit alongside it. Widow / orphan rules land in C.5.x.
- A block too tall to ever fit in the frame goes to `overflow`
with a matching warning rather than looping forever.
- Returns `{ pages, overflow, warnings, masterId, frame }`.
Pages are arrays of `{ blocks }`; each block carries
`{ kind, tokenType, lines, style, lineHeightIn, y, blockH }`
so the C.5.3 renderer can paint them straight onto a virtual
page.
The chapter editor toolbar now shows live word + page counts
("142 words · 4 pages") via the engine, plus a warning blurb +
hover-title with full details when something doesn't lay out
cleanly. Updates on every textarea keystroke (cheap path —
re-runs the engine for the active chapter only).
C.5.3 wires the engine to actually paint paginated pages: tile
thumbnails for reflow segments + an optional paginated preview
in the chapter editor's right pane.
Reflowable chapters take their first step. The Pages overview
now has a "+ Add chapter" button next to "+ Add page" / "+ Add
10 pages". Clicking it appends a `type: 'reflow'` entry to the
manifest, opens a fresh markdown editor pane, and seeds it with
"# Chapter N\\n\\nWrite your chapter here."
- Vendored `[email protected]` (~36 KB minified, MIT) as
src/vendor/marked.min.js. Loaded as a regular <script> next to
polygon-clipping and pdf-lib's future home.
- Reflow entries store `chapterMarkdown` locally; the
`textRef` / `textPinnedEventId` fields stay null until D1 maps
each chapter to a real NIP-23 article event. masterId defaults
to spec.defaultMasterId.
- New `#bookChapterEditor` two-pane view: textarea on the left
(autosaves to entry.chapterMarkdown via pushHistoryDebounced),
rendered HTML preview on the right via `marked.parse`. Mangling
+ header-id auto-generation off; <script> tags get
belt-and-braces stripped from the parse output.
- Body class `editing-book-chapter` flips the view: bookView and
the canvas chrome (sheet, palette, tools, layers panel) all
hide so the user sees only the editor. Shared
editing-book-page CSS already handled hiding canvas chrome via
the :not(.editing-book-page) selector, which I widened to also
:not(.editing-book-chapter) for #bookView.
- Toolbar: chapter title input (writes back to entry.chapterTitle
on input), "← Back to Pages" button (clears editingChapterId,
returns to the grid), and a pages-readout placeholder showing
"Pagination preview lands in Phase C.5.2".
- Clicking a reflow tile in the overview now opens the chapter
editor (was a no-op toast in B2). Esc returns to the overview
same as the page editor / master editor paths.
- syncBookChapterEditor() runs from render() so autosave restore
lands the user on the right pane without losing typed input
(textarea/title skip syncing when focused).
Phase C.5.2 wires the pagination engine and replaces the textarea
preview with a paginated stack of master-themed pages.
A new `text-frame` shape kind lets masters carry a reflow target —
the rectangle the Phase C.5 pagination engine will pour markdown
into.
- `defaultShapeData('text-frame')` returns the frame defaults:
padding 0.05 in, verticalAlign 'top', columns 1, styleOverrides
null. Matches the data-model spec in
docs/BOOK_DESIGNER_FEATURE.md.
- `buildShapeSvgInner` renders the kind as a dashed accent-blue
rectangle with a centered "Text frame" caption — non-rendering
in the final print output (transparent fill / dashed stroke);
the dashed outline is a positioning aid in the editor and gets
hidden by the engine once prose flows in.
- New "+ Text frame" button on the page-context bar, shown only
when editing a master (`editing-book-master` body class). Drops
a frame sized to the trim minus a 0.75 in margin so it reads as
a real "type area" out of the box; the user can resize / move
it like any other shape with the existing transform handles.
- `addBookTextFrameToActiveMaster()` snaps coordinates to the
canvas space (trim + bleed) so the frame lands inside the
printable area regardless of bleed.
Phase C4 ships the structural layer only — the frame doesn't yet
collect markdown. Phase C.5 vendors `marked`, adds the pagination
engine, and starts pouring chapter prose into these frames.
Pages inheriting a master now render its layers *under* their own,
in both the per-page editor and the Pages-overview tile thumbnails.
- New `effectiveMasterIdForPage(entryOrId)` resolves the page's
master: its own `masterId` override → book spec's
`defaultMasterId` → null. Setting `entry.masterId = null`
explicitly means "no master on this page" (overrides default).
- `isLayerForCurrentMode` in page-edit mode now also accepts the
page's effective-master target so those layers join the render.
- `renderLayers` uses an effective-z sort key that subtracts 1e6
from master-layer z values while in page-edit mode — guarantees
master layers DOM-precede page layers regardless of authored z,
so masters always sit beneath the page artwork.
- New body class `editing-book-master` (in addition to the
shared `editing-book-page`) lets CSS tell page-edit from
master-edit. CSS rule fades + click-throughs master layers only
in page-edit; master-edit mode keeps them fully interactive
(you're editing the master itself).
- Free-layer wrappers now carry `data-target` so the CSS selector
`[data-target^="book-master:"]` works without needing the
renderer to know about book mode.
- Pages-overview tile thumbnails composite the master layers
beneath the page's own (z forced to -1e6 + i for stable order)
so each tile shows the same composition the editor renders.
Phase C3 ships render-side composition; the per-page "Apply
master…" picker (entry.masterId override UI) is queued for the
next slice.
The Masters modal's "Edit" button (stubbed in C1) now opens the
chosen master on the existing per-page canvas. The shared
`editing-book-page` body class keeps the CSS that hides the
overview / shows the sheet + tool palette; the page-context bar
swaps copy and routing based on which surface is active.
- New `state.book.editingMasterId` field. Mutually exclusive with
`editingEntryId` — opening one clears the other.
- `openBookMaster(id)` enters master-edit mode and closes the
Masters modal so the canvas is unobstructed.
- `closeBookMaster()` clears the field and re-opens the Masters
modal — the user lands back where they came from.
- Canvas plumbing (currentDrawTarget, isLayerForCurrentMode,
templateLayerTargetsForMode, updateLayerTargetOptions) all
branch on editingMasterId vs editingEntryId so layers land on
the right surface and the layers sidebar shows the right
section header ("Master: NAME" vs "Page N").
- The Back-to-Pages bar relabels to "← Back to masters" + hides
prev/next chips when editing a master (no chapter walking on
masters). The same back button routes to the right close
function based on which mode is active.
- Esc and the same back button both work from either mode.
- updateBodyClasses() toggles `editing-book-page` on EITHER edit
field being set, so the CSS rules pick up master mode for free.
A new "Manage masters…" sidebar entry (was disabled in A2) opens
the Masters modal. The modal lists every entry in
state.book.masters and supports the usual CRUD:
- + New master: appends an entry with a generated id and the
label "Body pages". The first master created auto-promotes to
the default (state.book.spec.defaultMasterId).
- Rename: inline editing on the label input writes back through
pushHistoryDebounced so a long type doesn't spam the undo stack.
- Set default: explicit button on non-default rows; the active
default gets an accent-coloured border + "Default" badge.
- Delete: confirm + remove the entry + drop every layer under
`book-master:<id>` so the canvas doesn't carry orphans. Any
page that pointed at the deleted master falls back to the
default; if the deleted entry WAS the default, the next master
in the list inherits the role (or none, when the list empties).
- Edit (button stub, disabled): wired in C2 — it'll route to
master-edit mode where the canvas binds to `book-master:<id>`.
state.book.spec gains a new `defaultMasterId` field (a local id,
not a Nostr ref) so the spec block round-trips through autosave
without needing publish-time addresses. defaultMasterRef stays
null until D1 maps each local master to a real `casewrap-page`
event.
Esc closes the modal; click-outside-to-close too. Both match the
existing Document Settings modal lifecycle.
Each designed-page tile now renders a live preview of its current
layers, so the Pages overview reflects edits instead of showing
empty placeholders. Reflow tiles keep the striped placeholder
until C.5 wires the chapter renderer.
- New `.bookPageTileThumb` div fills the tile and hosts a
miniature canvas built via the existing
`renderCustomArtPreviewDOM` helper.
- Layers stored against `book-page:<entryId>` are cloned for the
preview with `target` swapped to `'canvas'` so the helper's
flat-canvas filter accepts them.
- Page background falls through: per-page `background` →
`spec.background` → `#ffffff`.
- Trim dimensions from `book.spec` drive the preview's canvas
aspect, so different book sizes get tiles with the right
proportions.
- Grid rebuild is skipped (grid emptied + early return) when the
overview isn't actually visible — book mode in page-edit mode
+ non-book modes both bail without paying the per-tile DOM
cost. The overview re-paints on `closeBookPage()` since render()
runs after.
position:fixed pinned the bar to the viewport center which drifted
over the sidebar header on wide layouts. Move the bar into a new
.canvasOverlayBars wrapper that shares grid-area: canvas with
#previewShell — the overlay is pointer-events:none with the bar
flipped back to pointer-events:auto. The bar is positioned absolute
within the overlay so it hovers over the canvas grid column without
being a child of the scrolling preview shell.
Switching the bars to position:fixed pinned them to the viewport
center — which on a typical wide layout drifted over the sidebar
header, looking sloppy.
New approach: move both bars into a new .canvasOverlayBars wrapper
that lives in the canvasArea grid as a sibling of #previewShell
with `grid-area: canvas`. Both grid items share the same cell and
stack; the overlay sits above with z-index 60. The bars inside it
are absolutely positioned to the overlay's top-center, so they:
- hover above the canvas-host (#previewShell)
- stay put when the canvas pans or zooms (the overlay isn't a
child of the scrolling shell)
- center on the canvas grid column, not the viewport — so the
sidebar / tool palette widths don't shift them
The overlay itself is pointer-events:none with children flipped
back to pointer-events:auto, so canvas clicks pass through except
where the actual bar pills sit.
#deckEditBar used position: absolute, which made it a child of the
scrolling #previewShell — panning or zooming the canvas dragged
the bar off-screen. Switching to position: fixed pins it to the
viewport so it stays put at top-center regardless of canvas pan or
zoom.
Both edit bars used position: absolute, which made them children
of the scrolling #previewShell — panning or zooming the canvas
dragged the bars off-screen. Switching to position: fixed pins
them to the viewport so they stay put at top-center regardless of
canvas pan / zoom. The horizontal sidebar offset at typical widths
keeps the pill visually centred over the canvas area.
The layers sidebar fell through to the case-cover sections
(Back / Spine / Front) when in book mode because
templateLayerTargetsForMode() had no book branch.
It now returns a single panel labeled "Page N" keyed to the
active entry's target (`book-page:<entryId>`), so the layers
listed and the Add-layer actions are scoped to the page the user
is editing. When no page is open (Pages overview is showing) the
helper returns an empty list and renderLayerList() naturally
renders nothing — consistent with the deck-sorter view that also
hides the layers fieldset until a slide is open.
The page-context bar landed in B2 but with plain inline styling, so
it sat as an invisible strip somewhere in the layout — Adam couldn't
find a way back to the Pages overview. Two fixes:
- Restyled #bookPageEditBar to mirror the deck-edit-bar floating
pill: absolutely positioned at the top-center of the canvas
area, accent-coloured "← Back to Pages" primary button, pill
prev/next chips with disabled states, page label in the middle.
Visible only when the editor is active (the existing
body.template-book.editing-book-page gate).
- Esc while editing a book page now also returns to the overview,
matching the slide editor's Esc-to-deck behaviour.
There's no explicit "Save" step — every edit autosaves through the
existing persistAutosave() path. Phase D1 will add an explicit
"Publish page as casewrap-page event" action; until then "Back to
Pages" is the way out, and the work is preserved on the manifest
entry's layers.
Adding a text layer on a book page kicked templateMode back to
'cover' and rendered the DVD case panels behind the book editor.
Three layer-creation helpers didn't know about book-* targets:
- applyTargetMode(target): the trailing else set templateMode =
'cover' for anything it didn't recognise. Book targets
(book-page:<id>, book-master:<id>, book-cover-*, book-spine)
now keep templateMode = 'book'.
- targetBounds(target): the switch fell through to sheetW/sheetH
for book targets, returning the wrong rectangle. Added a
book-* branch that returns the page's trim rect (inset by
bleed) for pages + masters, and the per-panel slice of the
spread for cover panels.
- updateLayerTargetOptions(): the #layerTarget <select> had no
option for the active book page. Book mode now renders a single
"Current book page" option keyed to the entry-qualified target;
if no page is open, a "No page open" stub keeps the select from
appearing broken.
Net effect: hit T, click on the page, type text — the layer lands
on the right page, the canvas stays on the book page, and the
sidebar target dropdown shows the right surface.