Commit Graph
100 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 4.7 961b24d0d8 feat(book): J1.b — distinct Pages-overview empty state when only chapters exist
The starter-template panel fired on '!pages.length' which is now
ambiguous: a user can have chapters (counted in pages[]) but no
designed pages, and the Pages tab needs to say something. Splits
into three cases:

  (1) no designed pages AND no chapters → starter templates (as
      before, with the hint updated to point at the new Chapters tab
      for + Add chapter)
  (2) chapters but no designed pages → 'No designed pages yet' panel
      with + Add page / + Add 10 pages / Go to Chapters buttons
  (3) at least one designed page → render the grid normally

Phase J1 polish — pure UI; data model unchanged.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 15:57:23 -04:00
thePR0M3TH3ANandClaude Opus 4.7 1e6f2e9a57 feat(book): Phase J1.a — Chapters tab + Pages/Cover/Chapters split
First step on the phase-j-linked-content branch. Splits the book
view's pages-and-chapters mash-up into three tabs (Pages / Chapters
/ Cover) without yet touching the data model or the pagination
engine. Foundation for J2..J6.

What changed:

- New 'Chapters' tab between Pages and Cover. Lists every reflow
  entry in book.pages as a row with title, source (naddr or local
  draft), size, pin / live / private pills, and per-row Edit / Pin /
  Delete actions.
- '+ Add chapter' button moved off the Pages overview's toolbar and
  onto the new Chapters tab. Pages toolbar now reads cleanly:
  Add page / Add 10 pages / Import page / Read / Export PDF.
- Pages overview hides reflow-entry tiles via a CSS rule
  (display:none on .bookPageTile.reflowSegment). Data still lives
  in book.pages — drag-reorder indices unchanged, pagination engine
  unchanged, PDF export unchanged. J2+ migrates the data out.
- Count readout updated from '0 pages · 0 reflow' to
  'N pages · M chapters' (uses 'design' count + 'reflow' count).
- defaultBookState + ensureBookState now scaffold empty links[] and
  threads[] arrays so the rest of the codebase can assume they
  exist. No code reads them yet.
- setBookSubView extended to handle three views, sets active class
  + display + repaints the active list. renderBookChaptersList
  hooked into the main render() loop only when its tab is visible.

Everything that worked before still works — chapter editor, publish
flow, PDF export, Reader Mode, all untouched. The visible change is
the new tab + the cleaner Pages overview.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 15:46:46 -04:00
thePR0M3TH3ANandClaude Opus 4.7 79c89101c4 docs: Phase J spec — Linked Content + threaded text frames
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 <[email protected]>
2026-06-04 15:36:45 -04:00
thePR0M3TH3ANandClaude Opus 4.7 3f5540712b fix(book): stop double-offsetting cover layers in the preview composite
The earlier cover-preview commit assumed layers were stored in
panel-local coordinates (0..trimW relative to their panel's trim
corner). They're not. Cover layers are appended directly to
#layerOverlay (renderLayers, ~line 22344: 'el.style.left = inches(layer.x)'),
which lives inside #sheet, so layer.x is sheet-absolute: a back
layer's x already starts at >= bleed, a front layer's x already
starts at >= bleed + trimW + spineW (targetBounds() seeds them
there at creation time). Adding backX / spineX / frontX in the
preview shifted every layer twice — front cover content slid off
the right edge of the canvas, spine content drifted onto front,
back content drifted onto spine.

Fix: just remap target to 'canvas' and pass x / y through. The
panel-offset variables are kept (renamed in scope) only for the
overlay's fold-line and label positioning, which IS in absolute
spread coordinates.

The 'spec backfill' from the prior commit still stands — defensive
against undefined coverHasBack / coverHasSpine on legacy state —
but the visible misalignment in the preview vs. the editor was this
coordinate-system mismatch.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 15:23:46 -04:00
thePR0M3TH3ANandClaude Opus 4.7 ef7e85a418 fix(book): backfill cover spec flags so older books don't drop back/spine layers
ensureBookState() now fills in any defaultBookSpec() fields missing
from state.book.spec. Older saved books that pre-date the
coverHasBack / coverHasSpine flags would otherwise restore with those
fields undefined, and the eleven '!!s.coverHasBack' / '!!s.coverHasSpine'
truthiness checks scattered across the cover-preview filter, sheet
sizing, renderDimensions, the cover edit slot layout, and the PDF
export pipeline would all read those as 'false' — silently filtering
back-cover and spine layers out of the spread.

User symptom: a cover with text + linked designs + a background shape
that spans the spread would render only the front panel's content,
because the back / spine layers were being dropped by the filter.
The spread width also collapsed to front + bleed, throwing off the
bleed visualization.

renderBookCoverPreview() now calls ensureBookState() before reading
the spec so the backfill runs even if no prior code path touched the
book this render cycle.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 15:10:19 -04:00
thePR0M3TH3ANandClaude Opus 4.7 842e672fc0 feat(book): load cover from Nostr or JSON (+ fix bookCoverPayload name collision)
Two new import paths for book covers:

- Load cover from Nostr: paste an naddr / nevent pointing at a
  casewrap-page cover-spread event. Pulls layers + spec hints (trim,
  bleed, hasBack, hasSpine) + the publish ref so the existing
  Re-publish cover flow keeps replacing in place.
- Load cover from JSON: file picker that accepts the format
  bookCoverFileExportPayload() emits. Applies layers + spec hints;
  clears book.cover.ref because the file isn't yet published.

Both ask before overwriting an existing cover and update the live
preview immediately.

The 'Replace cover from Nostr…' button was already in the HTML but
hard-disabled with a 'Phase D1 stub' note — now wired and re-labelled
'📥 Load cover from Nostr…'.

Also fixes a name collision from the prior JSON-export commit:
bookCoverPayload() was defined twice (Nostr publisher at line 11938,
file exporter that I added at line 26056). JS function hoisting let
the later one win, silently breaking publishBookCoverToNostr's
'page.layers' lookup. Renamed the file exporter to
bookCoverFileExportPayload() so both coexist.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 15:05:25 -04:00
thePR0M3TH3ANandClaude Opus 4.7 c66b1c03af fix(book): cover preview paints actual layers + book-aware project-title readouts
Two fixes wrapped together because they're both about the user
not seeing a meaningful preview in book mode.

1. renderBookCoverPreview() now paints the actual back / spine /
   front layers into a single composite canvas the size of the full
   wrap-around spread, using the same renderCustomArtPreviewDOM
   helper the Pages-overview tiles use. Each layer's target is
   remapped to 'canvas' and offset by its panel's position within the
   spread (back: x=bleed, spine: x=bleed+trimW, front: x=bleed+
   trimW+spineW). On top: a dashed-red trim guide, dashed fold-line
   dividers, subtle 'Back / Spine / Front' panel labels, and an
   empty-state hint when no cover artwork exists yet. The geometric
   panel-mock fallback is gone — the .bookCoverPanel CSS still ships
   but is now dead.

2. renderDimensions() no longer falls through to the DVD-cover
   else-branch in book mode. The 'Trim: 10.8 × 7.25 in / PDF page:
   8.50 × 11.00 in (portrait)' strings that showed for books were
   the DVD totalTrimW + state.trimH leaking through. A dedicated
   book branch now reads from state.book.spec and shows
   'Trim: 6 × 9 in / Spread: ... (with bleed)' when editing the
   cover, 'Page: ... (with bleed)' otherwise.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 15:00:26 -04:00
thePR0M3TH3ANandClaude Opus 4.7 faa4849523 fix(book): show page-edit bar in cover-edit mode + add Book/Meme/Other categories
- The bookPageEditBar CSS only matched .editing-book-page, so the bar
  (Back / Publish / Proof / Cover JSON) was completely hidden while
  editing the cover. Added .editing-book-cover to the selector so the
  bar shows in both modes.
- Back button label is now '✓ Done — back to Cover view' with a tooltip
  that says changes are autosaved. The old 'Back to Cover view' label
  hid the fact that work persists automatically.
- mediaCategories: added 'Book', 'Meme', 'Other'.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 14:54:59 -04:00
thePR0M3TH3ANandClaude Opus 4.7 6c33571329 feat(book): JSON export — whole book + cover-only
Two new payload builders:

- bookPayload() — captures spec, pages, masters, cover metadata,
  every layer that targets a book surface. Hooked into downloadProject
  so Save Project (JSON) in book mode now writes a real, round-
  trippable book file instead of falling through to coverTemplatePayload
  (which silently wrote a malformed DVD-cover JSON for books).

- bookCoverPayload() — cover-only payload with the three cover panels
  + the spec fields the cover needs to lay out correctly (trim, bleed,
  safe, spine width snapshotted at export so the file stays portable
  between books with different page counts). Surfaced via a new
  '💾 Cover JSON' button in the page-edit bar that only shows in
  cover-edit mode.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 14:50:29 -04:00
thePR0M3TH3ANandClaude Opus 4.7 9b5d6fa459 ui: disable trackpad two-finger swipe-to-navigate
Adds 'overscroll-behavior-x: none' on html / body so the browser
treats horizontal trackpad swipes as no-ops instead of back / forward
navigation. Vertical scroll is unaffected. Prevents the classic
'I lost my work because I scrolled the canvas' design-tool gotcha.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 14:43:02 -04:00
thePR0M3TH3ANandClaude Opus 4.7 3f5d244a8e fix(book): Edit cover now actually shows the cover scaffolding
Two bugs prevented the book cover editor from rendering anything
visible:

1. body.template-book had an unconditional 'display:none !important'
   on .coverOnly (and on .bleedFill, .trimArea, .foldLine, .spineText),
   so when editing-book-cover was set the slot boxes, fold lines, crop
   marks, and trim guides stayed hidden. The hide rule now uses
   :not(.editing-book-cover) for the cover scaffolding while still
   unconditionally hiding the disc/jewel scaffolding (which never
   belongs on a book cover).

2. The .slot positions are driven by --panel-w, --spine-w, --trim-h,
   --bleed, --safe CSS custom properties. renderDimensions() was
   writing DVD-cover state.* values into those for every render,
   including in book cover mode — so even if the slots were visible
   they would have been sized for the last DVD preset, not the book
   spec. renderDimensions now writes book-spec values into the CSS
   vars whenever state.book.editingCover is true, so the back / spine
   / front panels lay out at the book's trim with the spine derived
   from page count × paper thickness.

For the asymmetric cases (coverHasBack=false or coverHasSpine=false),
a new applyBookCoverSlotLayout() helper writes inline left / width
overrides for the slot, trim, fold line, and crop-mark elements so
they sit in the right places. clearBookCoverSlotLayout() drops those
overrides whenever we leave cover edit mode so the CSS defaults take
back over. New body classes no-cover-back / no-cover-spine hide the
slot box for the missing panel.

Slot placeholders ('Enter back image URL' etc.) are hidden in book
cover edit mode — those belong to the DVD URL flow, not the book
cover layer system.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 14:40:01 -04:00
thePR0M3TH3ANandClaude Opus 4.7 549d6a61d9 ui: 'Beta' badge on the Book layout tab + experimental banner in the Book sidebar
Surfaces that Book Designer is still under construction in two places:
- An amber 'Beta' pill on the Book layout-picker tab so users see it
  before they switch modes
- A full-width amber notice at the top of the Book sidebar fieldset
  that's always visible whenever the user is working in Book mode,
  with a short explanation of what works and what to expect

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 14:23:25 -04:00
thePR0M3TH3ANandClaude Opus 4.7 5706c4b21a docs: 'Copy as markdown' button in the docs modal header
Each docs page now has a header button that copies the raw markdown
source to the clipboard. Useful for pasting the docs into the Book
Designer chapter editor as test content.

- _docsCache now stores { html, md } so the raw source survives the
  marked.parse() pass without a re-fetch on copy
- _docsActiveMd tracks the currently displayed page's source
- Button shows a brief "✓ Copied" confirmation and routes through
  toastSuccess / toastError for status

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 14:14:08 -04:00
thePR0M3TH3AN 0f18580178 Merge book-designer: Book Designer feature + in-app docs + contextual help icons
67 commits delivering the Book Designer arc (phases A-I) plus the
in-app 'Read the docs' markdown viewer (30 pages across 5 groups)
and ~48 contextual ⓘ help icons knitted into tabs, layouts, panels,
tool sections, the pixel-art UI, welcome / community / tip modals.
2026-06-04 13:45:30 -04:00
thePR0M3TH3ANandClaude Opus 4.7 388ca5fce2 fix(docs-icons): use capture-phase delegation so clicking ⓘ inside a button doesn't fire the button's handler
Layout-tab buttons (mode-cover / mode-disc / etc) and app-mode tabs
(modeTabTemplate / modeTabDesigner) have direct click listeners that
fire during the bubble phase. The previous bubble-phase docs
delegation ran AFTER those handlers — so clicking the ⓘ inside a
layout tab would swap modes *and* open the docs.

Switching the docs handler to capture phase intercepts the click
before it reaches the wrapping button's bubble listener, and
stopPropagation prevents bubble delivery entirely. Now the ⓘ
opens docs without changing modes.

Verified with a slug-integrity sweep: every data-help attribute in
the UI resolves to a real _DOCS_TOPICS entry (no 404s).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 10:17:19 -04:00
thePR0M3TH3ANandClaude Opus 4.7 12b6beaffa docs: help icons in welcome / community / tip modals
Three more surfaces where docs help is high-value at the moment a
user is making a decision:

- Welcome modal h2 — opens the Overview docs page so first-run
  users can orient themselves before picking a starting layout.
- Browse Community modal h2 — opens the Publishing overview
  (public/private/premium comparison + tipping vs unlocking).
- Browse Community Premium tab — opens the dedicated
  Premium-publishing page so users browsing locked designs can
  read about how unlocking works before clicking ⚡.
- Tip modal h2 — opens the Zaps page; restructured the title
  area as a flex row so the icon survives the dynamic
  textContent updates that swap "Send a tip" vs other labels.

Coverage is now 48 ⓘ icons across the UI.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 10:13:18 -04:00
thePR0M3TH3ANandClaude Opus 4.7 4ff705709e docs: help icon in the pixel-art editor toolbar
Adds an ⓘ at the start of the pixel editor's tool bar (left of the
pencil/eraser/fill tool group) that opens the Pixel art docs page.
Pixel mode runs through its own bespoke toolbar (not the standard
left-sidebar fieldset flow), so the editor bar is the right home
for the icon.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 10:02:56 -04:00
thePR0M3TH3ANandClaude Opus 4.7 ca2a722656 docs: help icons on layout-picker buttons + book sub-surfaces
Each individual layout button now carries its own ⓘ so authors get
straight-to-the-page help even before picking a mode. Same delegated
click handler from the previous slice — the layout-tab's mode-switch
click and the icon's docs-open click coexist because the docs handler
stopPropagation's the event before the tab handler sees it.

Layout-picker buttons (8):
- Template tab: Case cover, Disc labels, Slide deck, Book.
- Designer tab: Custom art, Slide, Pixel art, Disc design.

Book sub-surfaces (4):
- Pages / Cover toggle bar — opens the Book page.
- Page editor top bar (book-page + book-master editing) — opens the
  Book page.
- Chapter editor toolbar — opens the Book page.
- Reader Mode header — opens the Book page.

CSS:
- .layoutTab .docsHelpIcon — smaller (.76rem) + reduced opacity by
  default; active state inverts colour to white-on-accent so the
  icon stays readable on a selected tab.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 09:59:09 -04:00
thePR0M3TH3ANandClaude Opus 4.7 ad39a72554 docs: contextual ⓘ help icons across the UI
Every major surface now carries a small ⓘ glyph that opens the in-
app docs modal directly to that surface's page. Single delegated
click handler on document.body (data-help attribute → openDocsModal
slug); zero per-icon wiring.

Placement (30+ icons):

- App-mode tabs — Template → template; Designer → designer.
- Sidebar legends (18) — Layout/Template/Canvas/Slides/Book/Deck
  theme/Artwork/Disc Layout/Jewel Inserts/Layers/Project metadata/
  Credits/Display options/Print & Save/Save & Export plus the
  Designer-tab equivalents — each linking to its matching page.
- Tool-options pane headings (6) — Pen tool, Direct Selection,
  Path actions, Pencil tool, Shape tool, Text tool — each opens
  its dedicated tool page.
- Modal titles (4) — Document settings, Export PDF, Master pages,
  Ready-to-publish — the publish-confirm h2 was restructured to a
  flex row so the icon doesn't get wiped by the JS that swaps the
  textContent.
- Settings → ⚡ Lightning wallet → opens the NWC setup page.

CSS:
- .docsHelpIcon — 16×16, muted color, inline-flex so it nests
  cleanly next to headings/labels. Hover brightens + scales 1.1.
- App-tab variant tones the colour down further (rgba on white +
  active states) so the icon doesn't compete with the tab label.

Behaviour:
- Cmd/Ctrl/Shift clicks pass through (let the browser handle if
  the icon is ever wrapped in a link).
- Each icon has role=button + aria-label + title for accessibility.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 09:54:53 -04:00
thePR0M3TH3ANandClaude Opus 4.7 7d63c353d5 TODO: surface industry-convention parity gaps in vector tools
The docs audit (commit ef03f73) caught several universal design-tool
conventions that aren't wired in the vector toolset — most are
single-branch additions to existing drag handlers. Capture them in
TODO.md under "Design-tool follow-ups" as a discrete cluster so we
remember they exist.

The 14 items:
- Select tool: Shift-constrain-axis on drag, Alt-drag duplicate,
  Shift-aspect-lock on corner scale, Shift-rotation 15° snap.
- Global: Cmd+A select-all, Spacebar temp-pan, Cmd+0 fit-zoom,
  Cmd+=/- zoom step.
- Shape tool: Shift-constrain to perfect primitive, Alt-from-center
  draw.
- Text tool: drag-to-size.
- Pencil tool: smoothing slider, Alt-drag append, auto-close near
  start.

Each entry notes rough scope so we can pick them off in any order.
None are blockers; the docs audit just reminded us they're missing.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 09:39:55 -04:00
thePR0M3TH3ANandClaude Opus 4.7 10a5022e9c docs: publishing tiers + zaps + NWC + JPEG/PDF fallback
Adds a "Publishing & payments" group covering the three tiers, the
Lightning plumbing, and how to wire a wallet up:

- publishing.md            — Overview of Public / Private / Premium
                             with a comparison table; how tipping vs
                             unlocking differs; replaceability +
                             republish-vs-fork.
- publishing-public.md     — Default tier. Event shape, tag layout,
                             discoverability, edit-vs-fork.
- publishing-private.md    — End-to-end encrypted to self via NIP-44
                             / NIP-04. AES-256-GCM pipeline, what stays
                             visible (pubkey, kind, d-tag), multi-
                             device decrypt via "Mine" tab, caveats
                             (no multi-recipient envelopes in v1).
- publishing-premium.md    — Zap-gated soft-gate publishing. Soft-gate
                             key derivation, watermarked preview,
                             zap-split tags (creator + platform),
                             buyer unlock flow, My Purchases vault,
                             honest framing about soft-gate limits.
- zaps.md                  — NIP-57 lightning payments. Tipping vs
                             unlocking. How to receive zaps (set
                             lud16 on profile). Fallback flow without
                             NWC (BOLT11 + QR + external wallet).
- nwc.md                   — Nostr Wallet Connect (NIP-47) setup.
                             CoinOS quickstart, supported wallets,
                             multi-device sync via NIP-44 encrypted
                             event, test-connection button, security
                             model (treat URI like a password + set a
                             budget).

Plus a "Why JPEG sometimes routes through PDF" section added to
panel-print-save.md, explaining the three fallback cases (CORS-
blocked images, tainted canvas, unsupported mode) and the auto-
switch to the print → PDF path that the browser's print engine
can handle.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 09:37:04 -04:00
thePR0M3TH3ANandClaude Opus 4.7 ef03f731d1 docs: audit pass — fix wrong shortcut + behaviour claims
Swept the help pages against the actual code paths and removed /
corrected claims that didn't ship.

vector-tools.md
- Removed: spacebar temp-pan, Alt duplicate-as-you-drag, generic
  Shift-constrain.
- Added: verified shortcuts (Cmd+Z/Y/D/C/X/V/G, Cmd+]/[ with
  Shift modifier, Cmd+J / Cmd+Alt+J in Pen edit mode,
  Delete/Backspace for layers + anchors).

tool-select.md
- Removed Cmd+A (not wired), Alt-drag-duplicate (not wired),
  Shift-constrain-axis drag (not wired), Cmd-click stack-walk
  (not wired). Kept the verified marquee + multi-select.

tool-direct-select.md
- Re-wrote anchor / handle interactions to match the actual hit-
  test branches (Alt-click anchor toggles kind in one click; Alt-
  drag a handle breaks symmetry; Alt-click segment inserts an
  anchor; plain drag of a segment bows it). Removed the "click an
  endpoint to continue drawing" claim — that's a Pen-only flow.

tool-hand.md
- Removed every fictional zoom shortcut (Cmd+wheel, Cmd+0,
  Cmd+= / Cmd+-). Documented the real zoom controls (−/+/Fit
  buttons + dropdown in the canvas toolbar). Spacebar reposition
  scoped correctly to the Pen tool only.

tool-pen.md
- Clarified that the Pen options pane has its own Fill / Stroke
  inputs (separate from any "Stroke panel" that doesn't actually
  exist).

tool-pencil.md
- Removed every claim that wasn't true: smoothing slider, Alt-drag
  append, auto-close. Pencil writes a plain polyline (M…L L L…),
  one layer per stroke. Tool options pane has only colour + width.

tool-shape.md
- Filled in the missing primitives (rounded-rect / circle /
  triangle — total 8, not 5). Removed Shift / Alt drag modifiers
  that aren't checked in startShapeDrag / commitShapeFromDrag.
  Added the small-drag → default-size fallback that the code does
  do.

tool-text.md
- Replaced the "drag a rectangle to size" claim with the actual
  click-to-place behaviour (placeTextWithTool drops a fixed-size
  box at the cursor + auto-flips to Select with the box focused).
  Softened the Cmd+B/I/U claims to "browser native rich-text
  shortcuts" since they're not wired in the app code.

tool-eyedropper.md
- Replaced the fictional "inline ⊙ buttons next to colour fields"
  with the real OS-colour-picker eyedropper that Chrome's native
  <input type="color"> shows.

panel-layers.md
- Removed Cmd+A select-all (not wired). Added Shift modifier to
  Cmd+]/[ for bring-to-front / send-to-back. Clarified that
  position units are inches in print modes, pixels in Custom Art
  / Slide / Deck (matches the unit toggle behaviour).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 09:32:11 -04:00
thePR0M3TH3ANandClaude Opus 4.7 e9f3682bff docs: sidebar-panel pages — left fieldsets + right Tool Options pane
Adds a "Sidebar panels" group to the in-app docs:

- panels.md             — overview + a table of every sidebar panel
                          with where it lives + a one-liner
- panel-metadata.md     — Project Metadata fields (title, category,
                          IMDb/UPC/TVDB/MBID/custom-tag/language)
                          + how they feed publish + designer-mode
                          equivalent
- panel-layers.md       — Layers panel — per-mode surface scoping
                          (Cover/Disc/Jewel/Custom Art/Slide/Deck/
                          Pixel/Book), per-row controls, reorder +
                          group + multi-select, layer-options
                          field list
- panel-export.md       — Export options panel — show/print toggles
                          for guides/bleed/safe/crops/fold, mode-
                          specific hides
- panel-print-save.md   — Save / Open project JSON + Print/Save-as-
                          PDF print-dialog tips + Export JPEG/PNG +
                          mode-specific gotchas
- panel-credits.md      — Credits panel — Linked Designs contributor
                          surfacing, tip button, publish-time tag
                          emission, private-publish behaviour
- panel-tool-options.md — Right-side floating Tool Options pane —
                          how it switches between tool options and
                          layer options, cross-links to per-tool docs

Registered as a new "Sidebar panels" group ordered after Vector
tools in the docs sidebar.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 09:08:27 -04:00
thePR0M3TH3ANandClaude Opus 4.7 2d201ee522 docs: per-tool pages for the vector-canvas toolbar
Adds a new "Vector tools" group to the in-app docs sidebar with one
page per toolbar tool + one page per supporting panel:

Toolbar tools (8):
- tool-select.md           — Select (V): move/scale/rotate whole layers,
                             multi-select, marquee, dive-into-path
- tool-direct-select.md    — Direct Selection (A): per-anchor edits,
                             handle break, segment-direct-edit
- tool-hand.md             — Hand (H): pan + temp-pan via Spacebar,
                             plus zoom controls (which work from any
                             tool)
- tool-pen.md              — Pen (P): full Illustrator parity —
                             Bézier draw, mid-draw spacebar reposition,
                             Alt handle-break, auto-add/delete anchors,
                             Cmd+J join, Cmd+Alt+J average
- tool-pencil.md           — Pencil (B): freehand → smoothed Bézier
- tool-shape.md            — Shape (S): rect/ellipse/polygon/star/line
                             primitives + Shift/Alt modifiers
- tool-text.md             — Text (T): rich-text layer + Type panel
                             + token notes
- tool-eyedropper.md       — Eyedropper (I): window.EyeDropper sample +
                             inline ⊙ buttons + browser-support notes

Supporting panels (3):
- pathfinder.md            — Unite/Subtract/Intersect/Exclude via the
                             vendored polygon-clipping
- align-distribute.md      — six align ops + four distribute ops, with
                             behaviour around locked layers
- free-transform.md        — multi-layer scale + rotate handles on
                             the selection bbox

Plus an overview page (vector-tools.md) with a keyed table of every
shortcut, a description of how the toolbar interacts with the Layers
panel, and the canvas-level keyboard shortcuts (Shift/Alt/arrow
nudges/Cmd+D/etc).

All linked into _DOCS_TOPICS as a new "Vector tools" group, ordered
after the existing Template-mode layouts section.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 08:59:47 -04:00
thePR0M3TH3ANandClaude Opus 4.7 23a16c8e89 docs: in-app "Read the docs" modal + per-mode help pages
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]>
2026-06-04 08:52:34 -04:00
thePR0M3TH3ANandClaude Opus 4.7 f50d92043c docs: add BOOK_DESIGNER_TUTORIAL.md
Hands-on end-to-end walkthrough for the Book Designer feature —
setup, starting a book (templates / deck-import / scratch), Document
settings, masters + text-frame, Pages overview, designed-page editor,
chapter editor (full feature exercise: tables, task lists, footnotes,
xrefs, {toc}, {index}, ^[term]), cover view, visibility chip,
publishing order, resolver behaviour, Reader Mode, embed integration,
PDF export modal, tokens reference, edge cases to probe, and a
codebase map.

Companion to docs/BOOK_DESIGNER_FEATURE.md (the spec).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 08:41:25 -04:00
thePR0M3TH3ANandClaude Opus 4.7 0651a3e4fc book(I4): footnote refs + chapter-end definitions (v1)
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]>
2026-06-03 18:46:45 -04:00
thePR0M3TH3ANandClaude Opus 4.7 5e3e7d1d7b book(I5): \{index}\ token + \^[term]\ inline markers
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]>
2026-06-03 18:45:33 -04:00
thePR0M3TH3ANandClaude Opus 4.7 f3d8135a9a book(I6): cross-references — [text](#chapter-N) → page numbers
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]>
2026-06-03 18:43:48 -04:00
thePR0M3TH3ANandClaude Opus 4.7 4b2f124fde book(I2.2): full table layout in pagination + DOM + canvas + PDF
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]>
2026-06-03 18:41:49 -04:00
thePR0M3TH3ANandClaude Opus 4.7 d16c475dca book(I3): \{toc}\ token — auto-generated table of contents
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]>
2026-06-03 18:36:49 -04:00
thePR0M3TH3ANandClaude Opus 4.7 e5721b67a7 book(fix): tile action buttons + page-number chip always on top
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]>
2026-06-03 18:35:42 -04:00
thePR0M3TH3ANandClaude Opus 4.7 5caf1d48cd book(I2.1): GFM task lists + nested lists in pagination
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]>
2026-06-03 18:32:58 -04:00
thePR0M3TH3ANandClaude Opus 4.7 a46d13f189 book(I1): CodeMirror markdown editor in chapter pane
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]>
2026-06-03 18:08:42 -04:00
thePR0M3TH3ANandClaude Opus 4.7 a0e84d50bc book(H1): per-page paywall toggle (gating intent)
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]>
2026-06-03 18:06:43 -04:00
thePR0M3TH3ANandClaude Opus 4.7 2f15bdca17 book(G2): embed \?book=<naddr>\ flip-book mode
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]>
2026-06-03 18:04:37 -04:00
thePR0M3TH3ANandClaude Opus 4.7 0d0c89da6a book(G1): Reader Mode — full-screen page-flip viewer
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]>
2026-06-03 18:02:48 -04:00
thePR0M3TH3ANandClaude Opus 4.7 ea3fa6f060 book(F3): import a single page from another book
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]>
2026-06-03 18:00:34 -04:00
thePR0M3TH3ANandClaude Opus 4.7 461ab85728 book(F2): deck → book conversion
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]>
2026-06-03 17:59:36 -04:00
thePR0M3TH3ANandClaude Opus 4.7 0567133e06 book(F1): starter book templates on empty Pages overview
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]>
2026-06-03 17:57:04 -04:00
thePR0M3TH3ANandClaude Opus 4.7 55e60150f0 book(E4): "Proof one page" quick export
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]>
2026-06-03 17:54:04 -04:00
thePR0M3TH3ANandClaude Opus 4.7 5ed606e3d0 book(E3): real PDF text for reflowed prose
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]>
2026-06-03 17:20:32 -04:00
thePR0M3TH3ANandClaude Opus 4.7 ef73d73152 book(E2): PDF export modal with bleed / crop marks / range / split
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]>
2026-06-03 17:16:56 -04:00
thePR0M3TH3ANandClaude Opus 4.7 f4243887cc book(E1): vendor pdf-lib + book PDF export skeleton
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]>
2026-06-03 13:32:20 -04:00
thePR0M3TH3ANandClaude Opus 4.7 55f53aeb49 book(D2 polish): manifest dirty tracking + feed-card preview
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]>
2026-06-03 13:27:52 -04:00
thePR0M3TH3ANandClaude Opus 4.7 5b19c25e2b book(D1.7): upstream-update chips on tiles + cover
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]>
2026-06-03 13:17:06 -04:00
thePR0M3TH3ANandClaude Opus 4.7 77514cf408 book(D1.6): localStorage disk cache for book sidecars
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]>
2026-06-03 12:41:46 -04:00
thePR0M3TH3ANandClaude Opus 4.7 4cabc7a938 book(D1.3): editable cover view + publish
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]>
2026-06-03 11:55:03 -04:00
thePR0M3TH3ANandClaude Opus 4.7 dc0a5ad20e book(D1.5): Linked Designs resolver for book sidecars
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]>
2026-06-03 11:08:40 -04:00
thePR0M3TH3ANandClaude Opus 4.7 988f25c754 book(D3): private publishing for sidecars
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]>
2026-06-03 11:00:12 -04:00
thePR0M3TH3ANandClaude Opus 4.7 b60acec93b book(D2): publish casewrap-book manifest
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]>
2026-06-03 08:49:27 -04:00
thePR0M3TH3ANandClaude Opus 4.7 c329eb4839 book(D1.2): publish page + master as casewrap-page events
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]>
2026-06-03 08:45:45 -04:00
thePR0M3TH3ANandClaude Opus 4.7 be5fe7bf18 book(D1.1): publish chapter as NIP-23 article
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]>
2026-06-03 08:43:21 -04:00
thePR0M3TH3ANandClaude Opus 4.7 af3cdf7af0 book(C.5.7): NIP-23 chapter import + pin/live toggle
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]>
2026-06-03 08:38:00 -04:00
thePR0M3TH3ANandClaude Opus 4.7 3a97f54ea0 book(C.5.6): per-tile master picker on Pages overview
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]>
2026-06-03 08:34:44 -04:00
thePR0M3TH3ANandClaude Opus 4.7 d24c55ded7 book(C.5.5): textStyles editor in Document settings
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]>
2026-06-03 08:33:10 -04:00
thePR0M3TH3ANandClaude Opus 4.7 f2252230fa book(C.5.4): substitute {page-number}/{chapter-title}/etc tokens in master text layers
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]>
2026-06-02 21:34:42 -04:00
thePR0M3TH3AN d500bcaf83 book(C.5.3): paint paginated pages into reflow tile thumbnails
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.
2026-06-02 21:26:03 -04:00
thePR0M3TH3AN b12c3cb625 book(C.5.2): pagination engine + page-count readout
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.
2026-06-02 21:24:15 -04:00
thePR0M3TH3AN f41e72e942 book(C.5.1): vendor marked + Add chapter button + markdown editor
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.
2026-06-02 21:17:38 -04:00
thePR0M3TH3AN 4e75db99b8 book(C4): text-frame layer kind (structural rectangle on masters)
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.
2026-06-02 21:12:19 -04:00
thePR0M3TH3AN f28f0d92e8 book(C3): master-layer underlay on page editor + composited thumbnails
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.
2026-06-02 15:23:13 -04:00
thePR0M3TH3AN 5796f5d5bc book(C2): master-edit mode (canvas binds to book-master:<id>)
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.
2026-06-02 15:20:10 -04:00
thePR0M3TH3AN 0b4c0ffd6e book(C1): Masters list + Masters modal (CRUD + default)
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.
2026-06-02 15:17:38 -04:00
thePR0M3TH3AN d08af68e12 book(B3): live page thumbnails in Pages overview
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.
2026-06-02 15:14:20 -04:00
thePR0M3TH3AN 95c90ed4c0 Merge branch 'main' into book-designer
# Conflicts:
#	src/index.html
2026-06-02 15:10:49 -04:00
thePR0M3TH3AN bb48922b68 center Back-to-Deck bar over the canvas column, not the viewport
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.
2026-06-02 15:09:21 -04:00
thePR0M3TH3AN 3f3073ed97 center edit bars over the canvas column, not the viewport
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.
2026-06-02 15:08:17 -04:00
thePR0M3TH3AN 37d195a4a2 Merge branch 'main' into book-designer 2026-06-02 15:04:15 -04:00
thePR0M3TH3AN e8df17bb5a pin Back-to-Deck bar to the viewport
#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.
2026-06-02 15:04:08 -04:00
thePR0M3TH3AN d26fd94fe3 pin Back-to-Pages + Back-to-Deck bars to the viewport
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.
2026-06-02 15:03:20 -04:00
thePR0M3TH3AN 38f28fb397 book: layers panel scopes to the active book page
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.
2026-06-02 15:00:17 -04:00
thePR0M3TH3AN 0a1ff5f8da book(B2): make the per-page edit bar discoverable
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.
2026-06-02 14:57:57 -04:00
thePR0M3TH3AN 01ecb8a85b book: stop layer-creation paths from bouncing book mode to cover
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.
2026-06-02 14:55:42 -04:00
thePR0M3TH3AN 70b270748e book(B2): per-page editor + page-context bar
Clicking a designed-page tile in the Pages overview now opens that
page in the existing Custom Art canvas, bound to the entry-qualified
`book-page:<entryId>` target so layers stay scoped to one page
across the manifest.

Plumbing:
- `state.book.editingEntryId` carries the active page id. Mirrored
  to the `editing-book-page` body class via updateBodyClasses().
- `currentDrawTarget()` returns `book-page:<editingEntryId>` when
  the book editor is active, so new layers drop on the right page.
- `isLayerForCurrentMode()` only accepts targets prefixed with
  `book-page:<editingEntryId>` (or matching master target) — pages
  don't leak each other's layers, and the Pages overview shows no
  canvas layers (it doesn't render the sheet anyway).
- New CSS: `body.template-book:not(.editing-book-page)` keeps the
  Pages / Cover view visible and hides the sheet + tool palette.
  Flipping to `.editing-book-page` restores them, mirroring the
  deck-sorter's pattern for slide editing.

UI:
- `openBookPage(entryId)` enters the editor; default tool resets to
  Select so users don't land mid-pen-draw.
- `closeBookPage()` (also `← Back to Pages` button) returns to the
  overview.
- `gotoBookPageRelative(±1)` walks only designed-page entries —
  reflow segments aren't editable on the per-page canvas, they open
  in the markdown editor that ships in C.5.
- Page-context bar above the canvas shows `Page N / M [· label]`,
  prev / next arrows (auto-disable at the ends), and the back button.
- Bar visibility is gated by both `template-book` and
  `editing-book-page` so it doesn't leak into other modes.

Reflow tiles still render in the overview (struck through the spec
in A4) but clicking them surfaces "Reflow chapter editor lands in
Phase C.5." instead of opening the canvas. That keeps the routing
honest until C.5 lands.
2026-06-02 14:52:41 -04:00
thePR0M3TH3AN 2df533a93d book(B1): drag-reorder + duplicate on Pages overview
- Tiles are now draggable; grabbing one and dropping on another
  swaps it into that position. Visual feedback: source tile fades
  to 50% opacity while dragging; the hovered drop target shows an
  accent-coloured outline.
- New `moveBookPage(from, to)` splices the manifest array and
  pushes a single history entry; nothing else changes (anchors
  reference entry ids, so reordering doesn't break refs).
- Per-tile hover overlay now has two actions instead of one:
  - `⎘` Duplicate — clones the entry with a fresh id, drops any
    anchor (the duplicate sits inserted-position by default), and
    appends "copy" to the label.
  - `×` Delete — unchanged, plus a soft danger hover state.
- `attachBookPagesDnd()` is idempotent (guards via
  `data-dnd-attached`) so re-rendering the grid doesn't pile up
  listeners.
2026-06-02 14:49:36 -04:00
thePR0M3TH3AN 382165d21b book(A5): Cover view with spec-derived spread preview
Cover view shows a live, proportional preview of the front / spine
/ back spread alongside a readout of every spec value that produced
it. Panel widths track the book.spec values exactly:

- Spine width derives via the existing bookSpineWidthInches()
  helper — page count × paper thickness when spineFromPages is on,
  otherwise the manual override. The summary readout marks the
  derived value in accent color with a "derived: N pages × T in"
  caption.
- The spread reflows automatically: change paper thickness in
  Document settings or add / remove pages, the spine column
  resizes, the readout updates. Same when toggling coverHasSpine
  or coverHasBack — the spread becomes back+spine+front /
  spine+front / back+front / front only.
- Bleed adds to each outer edge of the back and front panels; the
  inner edges between panels share so no double-bleed.
- A scaled px-per-inch factor fits the spread into ~720 px wide;
  scaling caps at 60 px/in so a tiny chapbook isn't huge.
- Cover view's renderBookCoverPreview() is guarded inside render()
  — it only repaints when the Cover sub-view is visible, so flipping
  between Pages and Cover stays cheap.

Phase A5 ships preview-only. Phase B mounts the existing cover
designer host inside #bookCoverView so users can actually paint the
panels; Phase D1 publishes the cover as its own casewrap-page event
with role:'cover-spread'. A "Replace cover from Nostr…" button is
stubbed disabled to anchor the future picker in the UI.
2026-06-02 14:45:21 -04:00
thePR0M3TH3AN 609ffbb6af book(A4): Pages overview view skeleton
The book mode now shows a dedicated Pages overview replacing the
empty cover canvas. Layout mirrors the deck-sorter pattern:

- New #bookView container in the canvas area, hidden by default and
  shown via `body.template-book`. Replaces #sheetWrap in book mode
  (same trick the deck sorter uses).
- View-toggle bar at the top: **Pages** / **Cover** with an active
  pill. Cover view is a placeholder pointing at Phase A5.
- Pages view has a header buttonRow with `+ Add page` and `+ Add 10
  pages`, plus a live page-and-reflow-segment counter readout.
- Tiles render as 2:3-aspect placeholders with the page number
  (driven by `book.spec.pageNumbering` — `enabled`, `startAt`,
  `firstNumberedPage`, `format` all honoured) and the entry's
  label. Reflow-segment tiles get a diagonal-stripe background so
  they're visually distinct from designed-page tiles (already
  positioned for Phase C.5 even though no reflow entries can be
  created yet).
- Per-tile delete button (visible on hover) removes the entry from
  the manifest; any other entry that anchored to the deleted one
  (`anchor.entryId` / `anchor.after`) gets its anchor reset to
  null so dangling refs don't break later phases.
- Empty state when `book.pages` is empty.
- `makeBookEntryId()` generates stable opaque ids
  (`entry:<random>`) so AnchorBlocks survive reorders, matching the
  data-model contract in docs/BOOK_DESIGNER_FEATURE.md.
- Hides the tool palette, canvas toolbar, and tool options panel
  in book mode (nothing to draw on yet — Phase B's per-page editor
  will surface them again when a page is open).
- `renderBookPagesGrid()` hooks into the main `render()` flow so
  the grid stays in sync with state across history undo / autosave
  restore / document-settings changes.
2026-06-02 14:42:11 -04:00
thePR0M3TH3AN 8fca55ff35 Merge branch 'main' into book-designer 2026-06-02 14:38:48 -04:00
thePR0M3TH3AN b829935103 fix: recreate <defs> in rebuildClipDefs when it's missing
Adam hit a hard crash every time he clicked a layout tab on the
deployed site:

  Uncaught TypeError: Cannot set properties of null (setting
  'innerHTML') at rebuildClipDefs (16403) → render (11689) →
  applyLayout (28513)

rebuildClipDefs() does defs.innerHTML = '' on an element found
via defsRoot.querySelector('defs'). If the SVG #globalClipDefs
container exists but its inner <defs> child has been removed (or
an early DOM-hook / browser extension injected the SVG without a
<defs> child), the query returns null and the assignment throws.
Because rebuildClipDefs is the first thing render() calls, the
crash aborts the entire render — body classes don't swap, the
canvas doesn't redraw, the layout panel doesn't update. The
top-level tab visually toggles but everything downstream stays
frozen. Exactly the "canvas stuck" report.

Guard the lookup: if no <defs> child exists, re-create one and
append it to the container, then proceed with the rebuild. The
ensureGlobalClipDefs() helper already creates a <defs> on first
call; this fix handles the rarer case where the container exists
in a malformed state.
2026-06-02 14:38:42 -04:00
thePR0M3TH3AN 7fcc8458be book: hide cover-mode canvas panels in book mode
The cover designer's BACK / SPINE / FRONT slot placeholders +
bleed / trim / fold-line / crop-mark guides were still rendering
on the canvas-host when book mode was active, because the existing
mode-switch only hides them for customart / slide / pixelart /
deck. Add a matching `body.template-book` block that hides every
`.coverOnly` element plus the disc / jewel artifacts and the
spine-text label.

Also hides the sidebar #exportFieldset / #layersFieldset /
#printExportRow until book pages exist (matches the deck-mode and
pixelart-mode treatment — they're not in the book WORKFLOW_ORDER
anyway, but the CSS gate keeps them from sneaking in).

The canvas-host now renders empty in book mode — the Pages
overview view (A4) and Cover view (A5) fill it in subsequent
commits.
2026-06-02 14:21:08 -04:00
thePR0M3TH3AN 57897a462b book(A3): Document settings modal
Wires the full Document settings modal for book mode. Every field
from `book.spec` is editable, applies live on Apply, and persists
via the existing autosave.

- New `#bookDocumentSettingsModal` markup:
  - Book size dropdown with five named presets (US Trade, US
    Letter, A5, A4, Square 8×8) plus Custom; selecting a preset
    locks the trim inputs to the right dimensions, Custom unlocks
    them.
  - Trim width / height (inches).
  - Bleed (default 0.125).
  - Paper thickness (in / page).
  - "Derive spine width from page count × paper thickness"
    checkbox; manual spine input shown only when unchecked. A
    live note below shows the computed spine ("Spine = N pages ×
    T in = D in").
  - Cover layout toggles: include spine, include back panel.
  - Page numbering: enabled toggle reveals start-at, first-
    numbered-page (manifest index), and a format string field
    with a `{n}` placeholder hint.
- New JS helpers:
  - `BOOK_SIZE_PRESETS` table (one source of truth for the five
    preset sizes).
  - `openBookDocumentSettings()` / `closeBookDocumentSettings()`
    matching the existing publish-confirm modal lifecycle.
  - `_syncBookSettingsSpineNote()` updates the derived-spine
    note on paper-thickness / from-pages toggle changes.
  - `_syncBookSettingsPresetLock()` locks / unlocks trim inputs
    when the preset dropdown changes.
  - `applyBookDocumentSettings()` writes every field back to
    `state.book.spec`, pushes a single history entry, then runs
    `syncInputs()` + `render()` so the sidebar summary refreshes.
- Sidebar "Document settings…" button is now enabled (was
  disabled placeholder in A2) and wired to the new modal.
- Esc-to-close hooked into the existing global Escape handler
  next to publishConfirm. Click-outside-to-close handled by the
  same delegation pattern other modals use.
2026-06-02 14:17:16 -04:00
thePR0M3TH3AN 7bed2a6a3f book(A2): Template-tab "Book" entry + mode switching
Adds the Book button to the Template tab's layout picker
(alongside Case cover / Disc labels / Slide deck) and wires the
applyLayout('book') path so the user can switch into book mode.

- `mode-book` button + click binding via the existing applyLayout
  loop (one-line list addition keeps every layout tab routed
  through the same handler).
- `applyLayout('book')` calls `ensureBookState()` to lazy-init the
  manifest the first time a user enters the mode.
- New `template-book` body class + CSS gating: the new
  `#bookFieldset` sidebar block shows only in book mode, and the
  cover / disc / deck / custom-art fieldsets hide in book mode so
  the sidebar stays focused on the spec + masters affordances.
- `WORKFLOW_ORDER` learns the book column.
- `sheetW` / `sheetH` fall back to trim + bleed when
  `templateMode === 'book'` so the canvas-host placeholder is
  sized correctly (it stays blank in this phase; the actual Pages
  overview + Cover view come in A4/A5).
- Sidebar fieldset shows a derived spec summary (trim, bleed,
  paper thickness, derived spine width, page + master count)
  rendered by a new `syncBookSidebarSummary()` hook into
  `syncInputs()`. Book-state helpers (`bookPageCount`,
  `bookSpineWidthInches`) are pure functions so future phases can
  reuse them.
- Two placeholder buttons (Document settings… / Manage masters…)
  are stubbed disabled with a hint pointing at the upcoming
  phases, so users can see where the controls will land.
2026-06-02 14:14:32 -04:00
thePR0M3TH3AN 314b582b0c book(A1): state shape + autosave round-trip for templateMode 'book'
First commit of the Book Designer feature. Pure plumbing — no UI
yet. Establishes the foundation for Phase A:

- New `templateMode: 'book'` recognised by `normalizeTemplateMode`
  (matches either `payload.templateMode === 'book'` or the presence
  of a `payload.book` block).
- `templateTypeForMode` returns `'book'` for the new mode.
- `primaryTargetForMode` returns `'book-page'`.
- `isLayerForCurrentMode` accepts every `book-*` target
  (`book-page`, `book-master`, `book-cover-front/back`, `book-spine`)
  in book mode.
- `state.book` initialised to `null`; the new `ensureBookState()`
  helper lazily fills it with `defaultBookState()` (empty spec,
  null cover, empty masters + pages arrays).
- `defaultBookSpec()` returns the v1 defaults documented in
  docs/BOOK_DESIGNER_FEATURE.md (US Trade 6×9, 0.125 bleed,
  paperThickness 0.0042, spineFromPages true, page numbering on,
  default master null).
- `defaultBookTextStyles()` returns the v1 markdown stylesheet
  (Georgia body, Helvetica headings, monospace code, accent-blue
  links, blockquote indented italic, etc.) — used by Phase C.5's
  pagination engine.
- SCHEMA_VERSION bumped to 6 with a comment explaining the
  additive `book` block. Existing autosave path serialises +
  restores `state.book` for free via Object.assign.

No behaviour change to existing modes — Book mode is unreachable
through the UI in this commit. The next commit (A2) adds the
Template-tab entry that switches into book mode.
2026-06-02 10:58:54 -04:00
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
thePR0M3TH3AN 7383b98cf9 docs(book): fold reflowable text + markdown + NIP-23 into the spec
Third revision pass adds reflowable prose as a first-class part of
the Book Designer, without giving up the fixed-layout design pages.

Architecture:
- Prose chapters live as NIP-23 long-form articles (kind-30023,
  markdown body). One source, two presentations: same article event
  is readable standalone on Habla.news AND laid out as designed
  pages inside a book on Artstr Studio.
- Book manifest's `pages` becomes a discriminated union:
  `{ type: 'design', ref }` for fixed-layout pages, and
  `{ type: 'reflow', textRef, masterRef, … }` for chapter segments
  that paginate dynamically.
- New `text-frame` layer kind, lives on masters. The pagination
  engine pours markdown tokens into copies of the master until the
  chapter is exhausted.
- New `spec.textStyles` block — a book-level stylesheet mapping
  each markdown element (h1-h6 / body / blockquote / code / list /
  link / hr) to typography (font, size, weight, leading, alignment,
  margins, plus keepWithNext / keepTogether flow controls).
- Anchored designed pages: designed entries on the manifest use
  `anchor.mode: 'before-chapter' | 'after-chapter'` so their
  semantic position survives reflow-driven page-count changes.

Tradeoff mitigations baked in:
- Pagination is fiddly → tight v1 markdown subset documented;
  pure-function engine for testability; "Preview pagination" before
  publish; overflow toasts for too-long words / wide code blocks.
- Text drift breaks pagination → chapters pin to a specific event
  id by default (safe for print); opt-in "Live" mode for
  always-latest with re-paginate notices showing page-count delta;
  anchor-based positioning for designed pages.
- PDF text fidelity → reflowed prose exports as real PDF text via
  pdf-lib font subsetting; font allowlist with vector-path
  fallback + per-export warning.
- Editor scope → plain textarea + live preview for v1; CodeMirror
  upgrade deferred to Phase I; WYSIWYG never in scope.

New tokens: `{chapter-title}`, `{chapter-number}` resolve per
chapter context (useful for running headers on masters).

Phases updated:
- Phase C grows `text-frame` layer kind (structural).
- New Phase C.5 — vendor `marked`, implement pagination engine,
  markdown editor pane, anchoring, pin/live tracking. Slots in
  before PDF export because PDF needs it.
- Phase D1 expanded to include chapter publish as kind-30023.
- Phase E updated for real PDF text on reflowed prose.
- New Phase I — extended markdown subset (tables, footnotes,
  auto-TOC via `{toc}`, cross-references) + CodeMirror editor.

Pages overview tile UX expanded: designed-page tiles show a
rendered thumbnail; reflow tiles show a stack-of-pages icon with
chapter title, master name, paginated page count, and a tiny
first-page preview.

Acceptance criteria for Phases C, C.5, D1, E updated to match.

TODO.md summary rewritten to reflect the new shape.
2026-06-02 10:47:42 -04:00
thePR0M3TH3AN 33ee2b7ec5 docs(book): rewrite spec around Nostr-native sidecar architecture
Apply twelve Nostr-driven revisions to the Book Designer spec:

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

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

TODO.md's Book Designer summary rewritten to reflect the
sidecar plan.
2026-06-02 10:36:40 -04:00
thePR0M3TH3AN 50eed8c017 docs: spec the Book Designer (unified cover + pages + PDF export)
New BOOK_DESIGNER_FEATURE.md drafts a top-level templateMode: 'book'
that orchestrates the existing cover designer, deck overview, and
custom-art canvas around a shared document spec (trim size, page
count, paper thickness, bleed). Spine width derives from page count
× paper thickness; the cover view goes read-only on the spine
column when in derived mode. Pages reuse the deck-sorter UI for
reordering; the per-page editor is the custom-art canvas bound to
a book-page:<id> target with prev / next page nav. Masters render
underneath each inheriting page and support {page-number} /
{page-count} / {book-title} token substitution.

PDF export ships in its own phase (E), backed by a vendored
pdf-lib (~80 KB). Single-file (cover + interior) and split (cover
PDF + interior PDF) output modes; bleed + crop-mark options;
v1 flattens text to vector for font-fidelity, with vector PDF
text as a follow-up.

Phased: A (spec + skeleton) → B (pages + editor) → C (masters) →
D (publish) → E (PDF export) → F (templates + deck-to-book
import). Non-goals: reflowable text, footnotes, automatic TOC,
CMYK / ICC, imposition, mixed page sizes in one book.

Linked from README's "Drafted-but-unbuilt" list and TODO.md.
2026-06-02 10:29:01 -04:00
thePR0M3TH3AN 1baa917c9a free transform: keep pointer capture during a rotate drag
startLayerRotate and startLayerSelectionRotate called render() on
every pointermove, which tore down the .freeLayer wrapper that owns
the rotation handle. That dropped pointerCapture mid-drag, so after
the first move event the gesture "let go" — the handle stopped
following the cursor and only a few degrees of rotation registered
before the user had to grab again.

Fixed both paths the same way the resize handler does: mutate the
layer's rotate (and centre, for multi-layer) and apply the change
directly to the affected .freeLayer's transform / position styles
inside move; only call render() on pointerup so the rest of the UI
catches up. The sidebar Rotation field updates live via
syncLayerInputs() as before.
2026-06-02 10:20:16 -04:00
thePR0M3TH3AN 31df555ae7 shape tool: rename tool-panel inputs so they don't collide with layer-panel IDs
The Shape tool's fill / stroke colour pickers and stroke-width input
shared DOM ids (shapeFillColor, shapeStrokeColor, shapeStrokeWidth)
with the selected-shape-layer edit panel's equivalents.
document.getElementById returns only the first match, so the input
listeners that drive _shapeToolOpts attached to the layer-panel
inputs (which aren't visible while the Shape tool is active), not
the tool-panel inputs. Result: changing fill / stroke colour or
stroke width on the tool panel had no effect on the next shape drawn.

Renamed the three tool-panel inputs to shapeToolFillColor /
shapeToolStrokeColor / shapeToolStrokeWidth (and their <label for>
references and the syncToolOptionInputs + input-event listeners).
A full repo scan confirms no other duplicate DOM ids remain.
2026-06-02 10:17:03 -04:00
thePR0M3TH3AN e25180346b Merge branch 'pen-tool-phase-d': Illustrator-parity pen / select tools, Pathfinder, Align, Eyedropper, Free Transform, Marquee
Bundles the design-tool polish arc:

- Pen tool: Direct Selection (A) as a separate tool, Cmd/Ctrl temp
  toggle, hover cursor cues, Shift constrain, auto-add / auto-delete
  on segment / anchor, click-vs-drag convert, Alt-drag asymmetric
  pull, Alt-click curved ↔ pointed (drops / grows handles), multi-
  anchor select via Shift-click + marquee, arrow-nudge anchors,
  drag-segment to bow the curve, continue-an-open-path from its
  endpoint, Spacebar reposition + Alt mid-draw asymmetric during
  create-drag, Alt-drag handle breaks symmetry in DSel, Cmd+J join,
  Cmd+Alt+J average, Path Actions side panel with batch buttons.
- Eyedropper (I) via native window.EyeDropper.
- Align / distribute panel for layer selection.
- Free Transform rotation handle on single + multi-layer selection
  bounding boxes, Shift snaps to 15°.
- Pathfinder ops (Unite / Subtract / Intersect / Exclude), backed
  by vendor/polygon-clipping.min.js.
- Marquee box-select on the V tool with marching-ants animation.
- Double-click a vector shape with V to dive into Direct Selection;
  DSel empty-click deselects the layer entirely.
- Misc: pen draft stroke width now matches the panel value, anchor
  + handle glyphs shrunk closer to Illustrator size, Direct Select
  arrow tinted light blue.
- Docs: PEN_TOOL_FEATURE.md + TODO.md updated.
2026-06-02 09:53:28 -04:00
thePR0M3TH3AN 5b0eec1a1a docs: log the Illustrator parity arc + follow-ups
PEN_TOOL_FEATURE.md gets a 2026-06-02 status block documenting
everything that landed on top of Phase D — Direct Selection split,
auto-add/auto-delete, click-vs-drag convert, Alt-click curved ↔
pointed, multi-anchor select / marquee / arrow nudge, drag-segment
bow, continue-endpoint, spacebar reposition, Alt mid-draw, handle
break, Cmd+J / Cmd+Alt+J, Path Actions side panel — plus the
adjacent design-tool features that shipped in the same arc
(Eyedropper, Align & distribute, Free Transform rotation,
Pathfinder, marquee box-select, dive-into-edit on dbl-click).

TODO.md gets a "Design-tool follow-ups" section listing the gaps:
Smart Guides, Gradients + shadows, Scissors / Outline text /
Command palette, Flip H / V (needs renderer scaleX/scaleY),
Pathfinder rotation support, pixel-art frame timeline.
2026-06-02 09:53:06 -04:00
thePR0M3TH3AN 404a6964a1 selection: dive-into-edit on shape dbl-click + DSel empty-click deselects
- Double-click a vector shape with the Select tool active and we
  flip to Direct Selection on that shape, auto-converting non-pen
  primitives (rect / circle / polygon / etc.) to editable pen paths
  first so anchors appear immediately — Illustrator's dive-into-
  isolation behavior. Double-click is ignored if any other tool is
  active or the click landed on a transform handle.
- Direct Selection on the canvas: a plain click in empty space
  (anywhere the overlay covers, including beyond the layer's bbox)
  now clears the layer selection entirely, not just the anchor
  sub-selection. The anchor-marquee path still kicks in for real
  drags. Off-canvas clicks already deselected via the previewShell
  handler, so this closes the last in-canvas gap.
2026-06-02 09:48:43 -04:00
thePR0M3TH3AN 8ef9b0cb34 pen tool: shrink anchor + handle glyphs closer to Illustrator size
The 0.0075 dot / 0.0018 line ratios still rendered as ~14px dots
with thick stems on a 1920px-wide preview, which dominated short
paths. Dropped the dot radius to 0.0028 and the line width to ~0.001
so the editing UI reads as a tiny on-canvas widget (~7px) rather
than a UI element fighting the artwork. Both renderPenEditHandles
and renderPenDraft use the new sizes so live drawing and post-commit
editing stay consistent.
2026-06-02 09:43:44 -04:00
thePR0M3TH3AN 5b1c038c27 tool palette: tint the Direct Selection arrow light blue
The hollow white arrow distinction was too subtle when both V and A
tools are side-by-side. Switched to a solid light blue (#60a5fa)
arrow so Direct Selection reads as visually distinct at a glance;
the regular Select arrow stays the default black.
2026-06-02 09:42:29 -04:00
thePR0M3TH3AN 2fc555a4ff pen tool: live draft stroke width matches the panel value
The pen draft path was using a canvas-relative thickness (lineW * 2,
derived from the canvas dimensions) for its preview stroke, so the
ribbon you saw while drawing looked different from the path you got
after committing. The committed path stores its stroke width in
pixel-space (1 unit = 1 px @ 96 dpi); the overlay renders in inch
space, so the live preview now divides _penToolOpts.penStrokeWidth
by 96 — same trick the pencil draft already uses — and adds round
line-cap / line-join so curves look consistent with the committed
path.
2026-06-02 09:40:59 -04:00
thePR0M3TH3AN cd54d8357f marquee box-select on canvas with the Select tool
Click-and-drag on an empty area of the canvas (with the V tool
active) now draws a marching-ants rectangle. On release, every
layer whose bounding box intersects the rectangle is selected;
Shift / Ctrl / Cmd held during the drag adds to the existing
selection instead of replacing it.

Plain clicks (no drag past the 4 px deadzone) still clear the
selection, preserving the previous "click empty area to deselect"
behaviour. Other tools own their own canvas drags (Pen creates
anchors, Hand pans, Shape drags out a primitive, Direct Select
marquees over anchors), so this only kicks in for V.

Shift+click on a layer to add or remove it from the current
selection was already wired — confirmed working through the
existing additive path in startLayerDrag.
2026-06-02 09:38:52 -04:00
thePR0M3TH3AN eb9aec19d8 pathfinder: Unite / Subtract / Intersect / Exclude on shape selection
Boolean ops on 2+ selected shape layers, backed by the vendored
polygon-clipping library (29 KB minified, MIT) loaded from
src/vendor/. Adds a new "Pathfinder" details block in the sidebar
under Align & distribute.

- Unite combines all selected shapes into one outline.
- Subtract removes upper shapes from the bottom-most shape.
- Intersect keeps only the overlapping region.
- Exclude keeps only the non-overlapping regions (XOR).

Pipeline: each selected layer's pen path is sampled to a polygon
(16 samples per Bézier segment, in world inches), the chosen op
runs, and the result is rebuilt as a new pen layer that inherits
the bottom-most input layer's z-order, target, fill and stroke —
matching Illustrator's Pathfinder convention. Non-pen primitives
(rect, circle, etc.) are auto-converted to pen paths first.

Limitations baked in:
- Rotated layers aren't supported yet (we'd need to rotate the
  sampled polygon before clipping). The op refuses with a toast.
- Curves are flattened — the result is a polygon approximation
  with `fill-rule: evenodd` so multi-ring outputs (donut from a
  subtraction) render correctly. The renderer learned to honor
  shape.fillRule for this commit.
- Multi-polygon results stay rendered correctly but only the
  first polygon's outer ring is preserved in pen.nodes, so
  Direct Select editing post-op is partial.
2026-06-02 08:54:26 -04:00
thePR0M3TH3AN 9bb401bea2 free transform: rotation handle on the selection bbox
A 9th handle (round, accent-colored, sitting above the bounding box
on a thin connector line) joins the existing 8 scale handles on both
the single-layer overlay and the multi-layer selection box.

- Single layer: drag rotates the layer about its own centre, with
  Shift snapping to 15° increments. The existing resize handles
  already account for layer.rotate in their local-frame math, so
  resizing keeps working at any rotation.
- Multi-layer selection: drag rotates each layer's centre around the
  selection bbox's centre AND advances each layer's own rotation by
  the same delta — the group spins as a rigid body. Pre-snapshotting
  the centres + rotations keeps the math stable across long drags.
- Locked / generated-disc layers are filtered out as everywhere else.
2026-06-02 08:45:51 -04:00
thePR0M3TH3AN ce1023a14e align + distribute panel for selected layers
A new "Align & distribute" details block in the layer-controls sidebar
surfaces eight ops: left / horizontal-centre / right and top / middle
/ bottom alignment, plus horizontal and vertical distribution.

- Multi-selection (2+ layers) aligns within the selection's own
  bounding box, so the outer layers stay put and the rest snap into
  line with them.
- Single-layer selection aligns to the layer's target bounds (page
  panel / disc face / custom canvas), making it easy to centre or
  pin a single asset without setting up a dummy second layer.
- Distribute requires three or more layers — the outermost two stay
  pinned and the inner layers slide so consecutive centres are
  evenly spaced along the chosen axis. Helpers toast a hint when the
  selection is too small.
- Locked / generated-disc layers are skipped automatically.

Flip H / V were prototyped here but pulled out — they need a renderer
extension to honour per-layer scaleX/scaleY, which is a separate change.
2026-06-02 08:43:39 -04:00