410 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 5 90ce6def59 Add archive & rebroadcast for relay migration
A salvage tool in Settings. Sweeps every Artstr design still readable,
saves the signed events to a JSON bundle, and re-publishes them to the
current relays so designs survive a relay going dark.

Reads as widely as possible: the user's relays plus the old defaults,
deliberately including relays marked dead, since one unreachable from
this network may answer from another and a dying relay is exactly the
one holding events worth rescuing.

Signed in, it also sweeps the signer's own events by author, which is
the only way to reach private and premium designs - those carry
randomised d-tags and no public tag. The pubkey is requested from the
signer directly rather than read from state.community.myPubkey: a
connected extension does not populate that, and during testing the tool
reported "0 of mine" while the key in fact had 233 authored designs, 18
of them encrypted. Every private design would have been skipped.

Two rules the tool will not bend, because getting them wrong is worse
than not running:

- Retractions are honoured. A blanket `#k: 30078` deletion filter came
  back with 670 unrelated kind-5 events from across the network, all of
  which would have been rebroadcast; the lookup is now scoped to the
  authors actually collected and further filtered to deletions that
  target this archive. Those go out first, so a relay learns a design
  was withdrawn before anything can put an older copy back.
- Only the newest revision of each addressable design is sent.

Publishing uses one socket per relay - publishToRelays opens a fresh
connection per event, which for 230 events across 6 relays would be
~1400 connections and immediate rate-limiting. Each event is awaited
individually with a bounded in-flight window and a single retry on
rate-limit, so every one is accounted as accepted / rejected /
unanswered.

That last part took three attempts. Fire-and-forget with an
idle-timeout completion check kept closing the socket on the unsent
tail once designs ran to megabytes and acknowledgements lagged the
sends - runs reported 26, 46 and 202 acceptances out of 230 while
claiming success. Verified against a local mock relay that simulates
rate-limiting: 230/230 accepted, 0 unanswered, relay-side counts
matching exactly, and end-to-end 238 found / 230 sent / 230 accepted.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-02 12:13:13 -04:00
thePR0M3TH3ANandClaude Opus 5 51b06a3052 Stop the community browser coming up empty
Three separate causes, one symptom.

1. Pixel-art import mode leaked its filters. Opening "Browse
   community..." from the pixel tool set the feed type filter to
   pixelart across every tab, and nothing ever put it back - closing
   the modal without picking a design left the ordinary community
   browser pinned to pixel art for the rest of the session, and left
   _pixelArtImportMode on, so the next design clicked anywhere got
   imported as a layer instead of opening. The mode now snapshots the
   user's own filters on entry and restores them on exit.

   The modal is closed from thirteen different places with no single
   close function to hook, so exit is driven by a MutationObserver on
   the modal's class list. Import mode cannot outlive the modal no
   matter which path closed it, including call sites added later.

2. Relay timeouts were never recorded as failures. queryRelays' timeout
   path called finish(false) without markRelayFailure, so a relay that
   accepts the socket and then goes silent never cooled down - it was
   re-queried on every single request and cost the full timeout each
   time. Only relays that errored or closed were ever penalised.

3. The default relay set had one working relay out of five. Measured
   against a casewrap query: damus refused the socket, nostr.band never
   answered, primal and snort returned a single event each. The entire
   feed rested on nos.lol, so one bad day there and the browser looks
   empty. Defaults are now chosen by measurement - nos.lol, nostr.mom,
   relay.nostr.net and nostr.oxtr.dev each returned a full page, with
   damus and primal kept on for publishing reach. Anyone still carrying
   the old list verbatim is migrated on load and their stale relay
   health cleared; anyone who edited their relays keeps their own.

Verified after the change: the feed pulls 89 designs in ~1.1s, damus is
correctly skipped while cooling down, and a cleared-storage first load
populates straight away.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-02 11:40:57 -04:00
thePR0M3TH3ANandClaude Opus 5 f8869368c9 Keep the background when importing delta-encoded GIFs
Frames after the first came in with the background punched out. The
cause is a common exporter pattern: mark the unchanged background as
the frame's transparent index so each frame compresses down to just
what moved, then tag every frame disposal 2. Those two are
contradictory - disposal 2 ("restore to background") is implemented as
a clear to transparent by every browser, which wipes the very pixels
the delta relies on showing through. The reported file also declares
its background colour as an unrelated cyan palette slot, so honouring
the spec literally is no better.

Suppress the disposal-2 clear in exactly one case: a full-canvas frame
in an animation whose first frame was fully opaque. Such a file
demonstrably has a baked-in background, so "restore to background"
cannot coherently mean "erase to nothing".

Both conditions are load-bearing, and each has a regression test:

- full-canvas only, so sprite erasure - disposal 2 on a small frame
  rect, the case the method genuinely exists for - still clears and
  leaves no trail over an opaque background;
- opaque-first-frame only, so animations on real transparency keep it.
  That includes our own exporter's output, which also writes
  full-canvas disposal-2 frames: without this condition a sprite
  re-imported from an Artstr GIF trailed its previous position.

Verified on the reported 640x640 40-frame GIF (every frame now solid,
100% opaque, black background intact) and on the transparent-background
Artstr icon animation (corners still transparent on every frame).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-02 11:14:36 -04:00
thePR0M3TH3ANandClaude Opus 5 8702043abf Import animated GIFs into the pixel editor
Add an inline GIF89a decoder - the mirror of the existing encoder, so
still no vendored dependency - and wire it into "From image / GIF...".
Every frame of an animated GIF becomes a frame in the pixel editor. A
browser <img> only ever exposes frame 0 to canvas, so decoding is the
only route to the rest; the decoder covers LZW, interlacing, local
colour tables, transparency and all three disposal methods.

The whole animation is quantized against one shared palette (per-frame
palettes made colours shimmer and burned through the 256-entry cap),
playback FPS is derived from the median GIF frame delay, and a Frames
control samples long animations down evenly. Frame count and working
resolution are bounded so a large GIF cannot exhaust memory, and
whatever gets dropped is reported rather than silently truncated.

Fix the reported pixel-editor import bugs:

- Dead canvas. applyLayout() tore the pixel editor down before its
  "layout unchanged" early return, and only the render() at the end
  rebuilds a standalone one - so clicking Pixel art while already in
  Pixel art left the grid on screen with nothing behind it and
  painting, the palette and import all silently did nothing.
- Locked W/H with no explanation, and a multi-image batch forced to the
  New frame target could never set dimensions at all. Batches can now
  target Whole sprite, and the lock states why and how to change it.
- Cover/Fit/Stretch previews were indistinguishable: the pixelized
  canvas rendered at its natural size (32 CSS px in a 250px box). Both
  panes now fill the box, and the source pane is framed exactly as the
  conversion frames it, so the two read as before/after.
- Photos were point-sampled on the way down, which threw away almost
  the whole image and read as noise. Area-average via progressive
  halving, switching to nearest at 1:1 and integer ratios so pixel-art
  sources stay crisp and import exactly.

Also fixed while verifying:

- Standalone pixel work was lost on refresh - pixelCommitIfStandalone()
  mutates state without calling render(), which is what writes the
  autosave. persistAutosave() existed for this case but was never
  called here.
- The quantizer always split to the requested colour count, padding
  small palettes with duplicate dead entries that cost event-size
  budget. It now keeps the source's exact colours when there are fewer
  than requested.
- sanitizePixelArtBlob truncated imported palettes at 64 though the
  model allows 256 (cells past the cut rendered transparent) and
  dropped per-frame durations.
- Stale grid-size pill after a resize, zoom not refitting when the grid
  grows, and a dead pixelImportFromImage() pointing at a nonexistent
  element.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-02 11:02:40 -04:00
thePR0M3TH3AN 20c769a97d Clarify premium creator ownership labels 2026-07-04 17:10:48 -04:00
thePR0M3TH3AN 00abaeb602 Alert creators when premium epoch refresh is needed 2026-07-04 16:42:14 -04:00
thePR0M3TH3AN b980215f15 Add creator premium epoch refresh action 2026-07-04 16:39:42 -04:00
thePR0M3TH3AN 37d871431b Make premium claim windows manual close by default 2026-07-04 16:34:23 -04:00
thePR0M3TH3AN e0e7755cf5 Document Premium 2.0 purchase flow 2026-07-04 16:16:03 -04:00
thePR0M3TH3AN bc35dd2cff Harden NWC split-zap timeout recovery 2026-07-04 16:08:28 -04:00
thePR0M3TH3AN 0729bce549 Add Premium 2.0 partial payment resume UI 2026-07-04 15:47:39 -04:00
thePR0M3TH3AN c9a8f63335 Add Premium 2.0 admin policy publishing tool 2026-07-04 15:44:16 -04:00
thePR0M3TH3AN d04a24d2e2 Add Premium 2.0 purchase repair workflow 2026-07-04 15:39:31 -04:00
thePR0M3TH3AN 8226e9a9ae Add Premium 2.0 legacy vault migration 2026-07-04 15:28:39 -04:00
thePR0M3TH3AN 0724709b0f Add Premium 2.0 private copy retry recovery 2026-07-04 15:20:48 -04:00
thePR0M3TH3AN d9f9084be6 Enforce Premium 2.0 claim windows 2026-07-04 15:16:20 -04:00
thePR0M3TH3AN 9c079dc402 Add Premium 2.0 admin policy publish stamping 2026-07-04 15:14:35 -04:00
thePR0M3TH3AN 170756f722 Wire Premium 2.0 private copy unlock path 2026-07-04 15:03:16 -04:00
thePR0M3TH3AN 92c41442dc Plan and scaffold Premium 2.0 purchase copies 2026-07-04 14:55:02 -04:00
thePR0M3TH3ANandClaude Opus 4.7 18dbe54a59 feat(book): J4.e — real PDF text for threaded text frames
PDF export now emits selectable + searchable PDF text on top of the
threaded-frame raster, the same way the existing reflow-entry path
does. Result: text in threaded frames is searchable in any PDF
viewer, copy-pasteable, and accessibility-tree-visible — not just
pixels.

What changed:

- _drawFrameLinesToCanvas accepts optional textOps + fontWarnings
  args. When provided, each rendered line gets pushed as a textOp
  describing {text, fontKey, sizePt, color, align, leftIn, widthIn,
  topInchesFromTrim}. Unsupported font families (anything outside
  pdf-lib's standard 14) are collected into fontWarnings.
- Empty / whitespace-only lines skipped (no glyphs to emit).
- renderBookDesignedPageToCanvas accepts textOps + fontWarnings and
  threads them to _drawFrameLinesToCanvas.
- exportBookToPdf populates textOps for designed pages too — same
  pattern the reflow branch already used. The existing pdf-lib
  drawText loop after the PNG embed handles both paths since the
  op shape is identical.

Limitations:
- Bold / italic per-line not yet supported — typography stores
  family + size + color + alignment + line-height + paragraph gap,
  but not weight or style flags. Adding weight: 'bold' / italic to
  the typography schema is a small follow-up.
- Only the 14 standard PDF fonts get real-text emission. Custom
  families fall back to raster-only (with a fontWarning surfaced
  in the export status).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-05 09:03:24 -04:00
thePR0M3TH3ANandClaude Opus 4.7 75fc29533e fix(book): J3-data — sweep the last inline nextFrame readers
Two stragglers from the migration commit:

- _bookPortOnClick read layer.nextFrame?.frameId to decide whether
  the click should break a thread. Now reads from the thread
  collection via _findThreadForFrame + _frameIndexInThread.
- The out-port tooltip read layer.nextFrame.frameId for the
  '⌗xxxxxx' destination hint. Switched to the pre-computed nextRef
  variable (already derived from threads[] earlier in the function).
- addBookTextFrame no longer initializes 'nextFrame: null' on new
  layers — that field is gone. Thread membership comes from
  book.threads[]; new frames are solo until linked.

After this sweep, no runtime code path reads layer.nextFrame. Only
the one-time migrator still touches it (intentional — it converts
legacy inline data into the collection then deletes the field).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-05 09:01:32 -04:00
thePR0M3TH3ANandClaude Opus 4.7 ee77820284 refactor(book): J3-data — threading moves from inline layer fields to book.threads[]
Source of truth for thread topology is now book.threads[] — a
top-level collection on the book manifest. Each thread:
  { id, sourceChapter, frames: [{ target, frameId }, ...] }

Layers no longer carry layer.nextFrame or layer.sourceChapter; the
thread collection is the canonical store. Aligns with the spec's
'events as linked files' model: thread graphs live in the manifest,
visible to the recipient, round-trippable.

What changed:

- New thread API: setFrameOutgoing, clearFrameOutgoing,
  setFrameSourceChapter, _findThreadForFrame, _frameIndexInThread.
  All write helpers handle the rebalancing dance (splitting threads,
  re-stitching, dropping empty single-frame threads).
- One-time migrator migrateBookThreadingToCollection — runs from
  ensureBookState on every state restore. Idempotent. Converts any
  legacy inline nextFrame / sourceChapter into book.threads[] then
  deletes the inline fields. Existing books migrate transparently
  the first time they're opened after the upgrade.
- _bookCollectThreadChain: now reads thread directly from
  book.threads[]. The backward / forward walk over inline links is
  gone — much simpler.
- _bookFrameChainContains: same — reads from threads.
- All readers updated to ask threads[] for the answer:
  _attachBookFramePorts (link state + chip), the editor canvas text
  painter, the tile thumbnail / PDF canvas pipeline, the Chapters
  tab Place button + Place helper, the inspector dropdowns +
  diagnostic status.
- All writers updated to call the new helpers instead of writing
  to layer.nextFrame / layer.sourceChapter:
    _completeBookPortLink → setFrameOutgoing
    _breakBookFrameLink   → clearFrameOutgoing
    inspector outgoing dropdown → setFrameOutgoing / clearFrameOutgoing
    inspector incoming dropdown → clearFrameOutgoing on old + setFrameOutgoing for new
    inspector chapter dropdown → setFrameSourceChapter
    Chapters-tab Place button + _placeChapterIntoSelectedFrame
    chip × clear button → setFrameSourceChapter(null)
- bookManifestPayload + bookPayload (whole-book JSON) include
  threads[]. Load path restores them.

The migrator + dual support means: legacy books in autosave/JSON/
Nostr round-trip cleanly. New writes go straight to threads[].

Cleaner manifest schema, smaller layer payloads, and the right
shape before merging to main.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-05 09:00:21 -04:00
thePR0M3TH3ANandClaude Opus 4.7 3d74689eb0 feat(book): J4.d.2 — per-frame typography + 144 dpi tile rendering
Two paired improvements:

1. Pages-overview tile thumbnails now render at 144 dpi instead of
   72. At typical grid widths (~150 px tiles) the 72 dpi raster
   shrank text to ~3 px tall — illegible. 144 dpi quadruples the
   pixel count per tile (~16 ms → ~60 ms per tile on the work I
   profiled informally) but text becomes readable. A content-hash
   cache is the next perf step if grids get large.

2. Text-frame typography is now per-frame, not per-thread. Each
   frame in the chain can override font / size / line-height /
   alignment / color / paragraph gap. The head's typography is the
   fallback when a frame doesn't set its own.

   - _paginateParagraphsIntoFrames reads typography fresh on every
     frame advance (and at paragraph start) so size differences flow
     correctly — when text crosses into a 14pt frame from a 10pt
     frame, line heights adjust mid-flow.
   - Painters (_paintFrameLines DOM + _drawFrameLinesToCanvas
     canvas) receive each frame's own typography rather than the
     head's.
   - Inspector typography controls are now editable on every frame
     (not just the head). Writes go to layer.typography on the
     selected frame. The status hint distinguishes head ('Applies
     to this frame. The head\\'s typography is also the chain\\'s
     fallback.') from mid-thread ('Applies to this frame only —
     overrides the head\\'s default.').

Known limitation: when a paragraph spills across a typography
boundary, the spilled lines were wrapped at the source frame's
width. They place + render at the destination's typography but
keep the source frame's line breaks. Re-wrap-on-advance is J4.d.3.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-05 08:47:10 -04:00
thePR0M3TH3ANandClaude Opus 4.7 4e60c020b0 fix(book): bumper padding under page-edit bar; reverts during print/export
The floating bar (Back · Publish · Proof PDF · Cover JSON etc) sits
absolute at top:10px and overlapped the top edge of the sheet
underneath. Added 60px top padding to .previewShell when the bar is
visible (editing-book-page OR editing-book-cover) so the sheet lands
below the bar.

Both @media print and body.exporting revert the padding back to the
24px default AND hide the bar entirely, so canvas exports and
browser-print outputs aren't offset by the on-screen clearance.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-05 08:25:15 -04:00
thePR0M3TH3ANandClaude Opus 4.7 ce1e1a783b fix(book): chain walk dropped frames past 2 — separate seen sets per direction
_bookCollectThreadChain shared a single 'seen' set between the
backward and forward walks. For a chain A → B → C started from C,
the backward walk visited B then A, leaving seen = {B.id, A.id}.
The forward walk from head A then tried to push B, but found B in
seen and broke immediately — returning chain [A] instead of
[A, B, C].

Symptom: cross-page flow worked for any 2-frame chain (forward
walk only had one node to visit), but broke as soon as the chain
hit 3 frames. The user repro 'A on page 1 → B on page 1 → C on
page 2 doesn't flow' matched exactly.

Fix: use separate seen sets per walk direction. Each starts with
the relevant endpoint (layer for backward, head for forward) and
detects same-direction cycles without bleeding into the other.

Affects every consumer of _bookCollectThreadChain: editor canvas
text rendering (_attachBookFramePorts), tile thumbnails
(renderBookDesignedPageToCanvas via the J4.c.2 hook), PDF export
(same path), and the text-frame inspector's chain / line-count
diagnostic.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 22:29:18 -04:00
thePR0M3TH3ANandClaude Opus 4.7 cbfed8702b diag(book): surface chain + line distribution in the text-frame inspector
When a text-frame is selected in a thread, the status line now shows
the actual paginator output:

  Frame 2 of 3 in this thread · 12 lines here; chain: [18 · 12 · 5]

That makes it obvious whether the chain walk found every frame and
how many lines landed in each. If the chain shows length 2 when the
user expects 3, the chain walk dropped a frame (probably a stale
nextFrame ref). If the chain looks right but line counts are wrong
in the 3rd frame, the bug is in the paginator.

Also logs a console.warn + appends '⚠ outgoing link is broken' to
the status when this frame's outgoing nextFrame ref points at a
frameId that doesn't exist in state.layers (deleted frame /
deleted page).

Diagnostic-only — no behavior change. Helps pin down the
'cross-page flow stops after 2 frames' bug.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 22:19:19 -04:00
thePR0M3TH3ANandClaude Opus 4.7 3d41eaaf7f fix(book): tile thumbnails now use the PDF-export canvas pipeline
Replaces the DOM-based preview helper for designed-page tiles with
renderBookDesignedPageToCanvas at 72 dpi. That's the SAME function
the PDF export + Reader Mode use, so the tile is guaranteed to
match the print output. No more drift on:

- Linked-design layers (async resolve was inconsistent in the DOM
  preview path)
- z-order between threaded text and overlapping layers
- Clip paths (DOM preview used a different clip serialiser)
- Group transforms
- Master text-frame outlines (already filtered in J6, but worth
  noting they're handled by the canvas path too)
- Master layer underlay z-ordering
- Token substitution in master text (canvas path runs
  applyBookTokensToLayers)

Each tile:
- Stage 1 (sync): paint the spec background colour so the grid
  doesn't shift while canvases render.
- Stage 2 (async): renderBookDesignedPageToCanvas → canvas.toDataURL
  → swap in as <img>. Guarded by isConnected so late completions
  don't overwrite a re-rendered tile.

Trade-off: each tile spins up its own async render. For typical
book sizes (10-100 pages) this is fast enough. A future cache by
(entry.id + content-hash) could skip re-rasterising unchanged
pages on every grid repaint.

The old _paintThreadedFramesOnThumb helper is now dead code
(canvas renderer handles threaded text internally) but left in
place; cleanup deferred.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 22:08:13 -04:00
thePR0M3TH3ANandClaude Opus 4.7 e6e1ef17f5 feat(book): Manifest Links panel in Document settings
Adds an InDesign-style 'Links panel' to the book's Document settings
modal — every external event the book references, with status.

Each row shows:
- Icon (🏷 cover · 🎨 master · 📐 page · 📄 chapter · 📚 manifest)
- Label + truncated coord (kind:pubkey…:dTag)
- Status pill:
    resolved  — sidecar data is in state (layers loaded / markdown
                fetched)
    🔒 private — encrypted ref (decrypt happens at resolve time)
    unresolved — ref set but no data fetched yet (try Refresh)
    unpublished — surface has layers but no Nostr ref (publish to
                  mint an event)
    local draft / empty — chapter content without a ref
- Actions: 📋 copy naddr to clipboard · ↻ force re-fetch this
  sidecar via resolveBookSidecars(..., { force: true })

Plus a 'Refresh all' button under the list that forces a full
re-resolve and repaints the panel when done.

Counts shown in the section header summary so you can see at a
glance how many references are resolved vs. pending.

Closes the loop on the Manifest Links design the user asked for —
this is the read-only view of what the casewrap-book event points
at, alongside the chip / refresh affordances that have been on the
sidebar and tile rows since D1.7.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 22:02:45 -04:00
thePR0M3TH3ANandClaude Opus 4.7 ff0b7348f2 feat(book): direct manifest import — from Nostr (naddr) or local JSON
Closes the loop on the manifest pattern. Two new buttons in the
book sidebar:

- 📥 Import book from Nostr… — paste an naddr / nevent at a
  casewrap-book event. The function resolves the event, tries
  NIP-44 / NIP-04 decryption (so private books work), feeds the
  payload through loadProjectFromText's existing book branch,
  injects the publish ref so future Publish replaces in place,
  then surfaces resolveBookSidecars summary (X / Y resolved, N
  failed) into #bookResolveStatus when it settles.

- 📁 Import book from JSON… — file picker that accepts the
  whole-book JSON Save Project writes (templateMode='book' +
  top-level book field). Same downstream path as Nostr import;
  also kicks off the resolver so any chapter-by-textRef refs
  populate.

Both reuse the existing book branch of loadProjectFromText (line
30441) — no new state-restore code; just clearer entry points for
discovery. Community-browser-driven loads still work; this is the
direct-paste / direct-file affordance the user couldn't find before.

Status reporting comes through #bookResolveStatus (the hint line
that already shows '↻ Refresh from Nostr' results) so users can
see progress without opening DevTools.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 21:59:06 -04:00
thePR0M3TH3ANandClaude Opus 4.7 a0e6329480 fix(book): Pages overview tile thumbnails — accurate to printed output
Three causes of inaccuracy in the thumbnail rendering, all addressed:

1. Bleed offset. Layer x/y are stored sheet-relative (include bleed
   inches). The tile canvas is sized trim-only (matches what gets
   printed). Result: every layer rendered shifted right + down by
   bleed/trim ≈ 2% for US Trade. Now offset each layer by -bleed when
   composing the tile preview — same translation
   renderBookDesignedPageToCanvas uses for PDF export.

2. Text-frame outlines. The dashed text-frame border is an authoring
   affordance, not a print element. The designed-page tile path now
   filters out shape layers with kind='text-frame' before passing to
   the preview helper (same filter the reflow-tile path already used
   on masters). Threaded chapter text still paints over the bare
   frame area via _paintThreadedFramesOnThumb.

3. Aspect ratio. Tiles defaulted to 2:3 in CSS — fine for US Trade
   6×9 but distorted for Square 8×8, A4, etc. Each tile now sets
   aspect-ratio inline from the book's actual trim dimensions.

Tiles should now match the PDF output much more closely.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 21:44:46 -04:00
thePR0M3TH3ANandClaude Opus 4.7 97597f1e3d feat(book): J4.d — line-by-line overflow + frame typography (strip markdown)
Two architectural shifts to match how design tools (InDesign, Affinity
Publisher, Scribus) handle threaded text.

1. Content vs presentation split. Chapter markdown is flattened to
   plain paragraphs on import — headings, bold, italics, list
   bullets, blockquote marks all stripped. The author writes in
   markdown because it's a good authoring format; the designer
   applies presentation on the frame, not in the source.

2. Line-by-line spill. The block-based engine moved whole paragraphs
   to the next frame when they didn't fit, leaving gaps at the
   bottom of each frame. The new engine wraps each paragraph against
   the current frame's width using its typography, then places
   lines one at a time — when the frame is full, the NEXT line
   continues in the next frame. Paragraphs break across frames
   gracefully.

What changed:

- _flattenChapterToParagraphs(md) strips markdown to plain text,
  collapsing whitespace and preserving paragraph breaks.
- _getFrameTypography(layer) returns the frame's typography (font,
  size, line height, alignment, color, paragraph spacing) with sane
  defaults.
- _paginateParagraphsIntoFrames(paragraphs, chain, typography) is
  the new line-based paginator. Wraps each paragraph using the
  active frame's inner width, fills the frame to height, advances
  to the next frame, repeats. Returns per-frame line buckets +
  hasMoreText flag for overset detection.
- _paintFrameLines / _drawFrameLinesToCanvas — DOM + Canvas2D
  rasterisers for the new format. Replace _paintFrameBlocks and
  _drawFrameBlocksToCanvas in the three render paths (editor,
  tile thumbnails, PDF export).
- addBookTextFrame seeds new frames with default typography
  (Georgia 11pt, 1.5 line-height, 0.1in paragraph gap, left align,
  near-black).
- Text-frame inspector adds a 'Typography' section: font, size,
  color, line height, paragraph spacing, alignment. Editing on
  the head propagates to the whole thread. Mid-thread frames show
  the read-only inherited values.

Existing reflow entries (chapters in book.pages used the
non-threaded way) still flow through the legacy block engine —
that path is untouched.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 21:39:37 -04:00
thePR0M3TH3ANandClaude Opus 4.7 be9a33a44c feat(book): text-frame inspector in the right panel — discoverable threading
Replaces the port-click dance as the primary path for threading +
chapter placement. Whenever a text-frame layer is the primary
selection in book mode, a new #bookTextFrameOptions section appears
in the right-side #toolOptions panel with:

- Status line: 'Frame N of M in this thread' / 'Solo frame' /
  '(Head — chapter source goes here)'.
- Chapter source dropdown — every reflow entry in book.pages. Only
  enabled when the selected frame is the head of its thread;
  mid-thread frames inherit and show a hint to that effect.
- Incoming-from dropdown — every other text frame in the book that
  could safely link TO this one (no cycle). Selecting one sets the
  source frame's nextFrame to point here; selecting (none) clears
  whatever was linking in.
- Outgoing-to dropdown — every other text frame this one could
  safely link to. Selecting one sets this frame's nextFrame;
  selecting (none) breaks the link.

Both dropdowns share _enumerateBookTextFrames, which labels each
candidate by its surface ('Page 3', 'Master · Standard interior') +
its ⌗frameId. Cycle detection reuses _bookFrameChainContains so the
dropdown only shows safe candidates.

Force-shows the outer #toolOptions panel when the inspector is
visible — the default toggling in syncToolPaletteUI hides it for
the Select tool, but with a text-frame selected we want the
inspector regardless of active tool.

Port-click + Chapters-tab 'Place' button still work as alternative
paths. The dropdown panel is the discoverable one.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 21:09:48 -04:00
thePR0M3TH3ANandClaude Opus 4.7 81dd6bf6b6 fix(book): text-frame ports were eating resize handles + drag/select events
Three reported bugs, one root cause: the in/out ports sat at the
top-left and bottom-right CORNERS of every text-frame layer —
exactly where the lh-nw and lh-se resize handles live — AND their
pointerdown bubbled to the freeLayer's startLayerDrag handler.

Symptoms:
- Couldn't resize a text frame (corner handles hidden under the
  larger-z-index ports).
- Couldn't click an in/out port to thread (the port's pointerdown
  started a layer drag, the click never fired).
- Couldn't 'Place chapter into selected frame' (the chapter button
  needs a selected frame, and selection requires a click that
  wasn't being shadowed by drag).

Fixes:
- Move ports OUTSIDE the layer's bounding box on the top edge
  (in port) and bottom edge (out port), centered. New tab shape
  with a dashed connector to the frame edge so the affordance
  reads. Both well clear of the 8 corner/edge/rotation handles.
- Bump the frameId badge to sit just above the layer (top-right,
  -16px) so it doesn't visually conflict with the in port.
- Add pointerdown + mousedown handlers on both ports AND on the
  source-chapter chip + its X clear button that stopPropagation.
  Drag handler no longer kidnaps the click.

Resize, select, link, and place-chapter all flow correctly now.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 20:59:28 -04:00
thePR0M3TH3ANandClaude Opus 4.7 10d9e51155 feat(book): J4.c.2 — threaded text exports to PDF on designed pages
renderBookDesignedPageToCanvas now walks page layers after the
master + page layers are drawn, finds text frames whose thread head
holds a placed chapter, paginates via _paginateChapterIntoThread,
and rasterises each frame's slice onto the export canvas at the
frame's coordinates.

New helper _drawFrameBlocksToCanvas — stripped-down counterpart to
the block-painting loop in renderBookReflowPhysicalPageToCanvas.
Handles paragraphs, headings, lists, blockquotes, code blocks, hr,
and plain text — same things the editor's _paintFrameBlocks paints
to DOM. Tables in threaded frames + real-PDF textOps emission (for
selectable text in the PDF) are deferred to a J4.b2 cleanup.

PDF output should now contain the threaded chapter text where the
user authored their frames, not just the dashed text-frame outline.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 20:34:46 -04:00
thePR0M3TH3ANandClaude Opus 4.7 9d8be7e0fe feat(book): J4.c.1 — threaded text shows in Pages overview tile thumbnails
After a designed-page tile is painted, walk its text-frame layers,
identify any that hold a placed chapter via their thread chain, and
paint each frame's paginated slice as an overlay at the frame's
position in the thumb canvas. Same _paintFrameBlocks helper the
editor uses, sized to the frame's bounding box.

The tile thumbnail now reflects the real text flow — author a
chapter, place it on a page-1 head frame, and the Pages overview
shows the first chunk of the chapter text in that frame; the
linked frame on page 2 shows the next chunk.

Still pending: PDF export and Reader Mode walk reflow entries the
old way (J4.c.2 / J4.c.3).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 20:33:26 -04:00
thePR0M3TH3ANandClaude Opus 4.7 282f5df4f3 feat(book): J4.b — variable-geometry thread pagination
Lets a thread have frames of different sizes and have text wrap
correctly in each. The chain {frame1: 6×4, frame2: 4×6, frame3:
6×6} now line-breaks at frame1's 6″ width, then at frame2's 4″
width, then at frame3's 6″ width — no clipping in the narrow
frame, no excess gutter in the wide one.

What changed:

- paginateChapter accepts opts.startTokenIndex and opts.maxPages.
  The token loop honors both: starts from the given index, stops
  after the page count is met, and returns result.endTokenIndex
  so the caller knows where to resume.
- _paginateChapterIntoThread walks the chain frame-by-frame,
  calling paginateChapter once per frame with that frame's
  geometry + the running token cursor. Each frame contributes its
  own one page to the chain.
- Overset detection switched from a page-count comparison to a
  token-cursor comparison (paginated.hasMoreText). More accurate
  with variable geometry where page count ≠ frame count.

Limitations still standing:
- PDF export, Reader Mode, and tile thumbnails still walk
  book.pages reflow entries the old way — J4.c.
- Tokens are re-lexed and re-measured per frame today. For long
  chains this is N × token-count work; cache by chain+chapter
  if it becomes a perf problem.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 20:31:57 -04:00
thePR0M3TH3ANandClaude Opus 4.7 2d7e757308 feat(book): J4 — thread-aware pagination, text flows through linked frames
Hooks up the threading wiring so chapter text actually pours through
the frame chain you authored in J3 + J5.

What changed:

- paginateChapter now accepts opts.frameOverride = { x, y, w, h,
  padding }. When provided, it skips the master-frame lookup and
  uses the override directly. Existing callers (master-driven
  pagination) keep working unchanged because the override is opt-in.
- New _paginateChapterIntoThread(headLayer, book) — builds a
  synthetic call to paginateChapter using the head frame's geometry,
  returns the pages + the resolved chain.
- New _paintFrameBlocks(el, page, frameW, frameH, padding) — paints
  one paginated page directly into a freeLayer wrapper. Like
  paintReflowPageOverlay but coordinates are frame-local instead of
  page-local.
- In _attachBookFramePorts: when a chip is being shown (chapter
  placed on the thread's head), paginate + paint the slice for THIS
  frame. If the chapter overruns the chain, the last frame gets a
  red ⊞ OVERSET badge. If the chapter is shorter than the chain,
  extra frames get a muted '(thread continues — chapter ended
  upstream)' placeholder.

You can now: thread frames, place a chapter on the head, and the
text actually flows across the chain. Real text, real wrapping,
real headings + lists + code blocks — same layout engine the
reflow-entry path has always used.

Limitation captured for J4.b: every frame in a thread shares the
head's geometry. Variable-geometry threads (e.g. front-matter frame
narrower than body frames) need the engine to walk tokens per-frame
instead of all at once. Today the line breaks are computed once at
head geometry and re-used.

PDF export, Reader Mode, and the per-tile thumbnail painter still
walk book.pages reflow entries the old way — they don't see threaded
frames yet. That's the J4.c cleanup once the engine settles.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 20:28:35 -04:00
thePR0M3TH3ANandClaude Opus 4.7 83d451dfe4 feat(book): J5 — place chapter into a thread's head frame (placeholder body)
Wires a text-frame layer to a chapter source. UX:

- Each chapter row in the Chapters tab gets a '📥 Place in selected
  frame' button. Disabled when no text-frame is selected, when the
  selected frame is mid-thread (must place on the head), or when the
  selected frame already holds this chapter.
- On click: head.sourceChapter = entry.id, history-push, re-render.
- Threaded text frames render a small blue chip up top showing
  '📄 <chapter title> · frame N / M', plus an × button on the head
  to unplace. The chip is identical across every frame in the chain
  so the chapter assignment is legible everywhere.
- Inside the frame: italic placeholder line ('(text will flow here
  — J4 pours it)'). Real text layout into per-frame geometry lands
  in J4.

The thread-walk uses _bookCollectThreadChain, so a placed chapter
is visible across all linked frames immediately. Unplacing from the
head clears it everywhere in the chain.

Mid-thread frames don't get a clear button — clearing on a non-head
would be ambiguous. The Chapters tab button refuses to place on a
mid-thread frame for the same reason.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 20:01:14 -04:00
thePR0M3TH3ANandClaude Opus 4.7 a419160b3a feat(book): J3.c — show-thread overlay on port hover
Hovering any in / out port of a threaded frame highlights every
frame in the same chain. The head of the thread (first frame, no
incoming link) gets a blue outline; the rest get green. Hover off
clears the highlight.

_bookCollectThreadChain walks backwards along incoming nextFrame
refs to the head, then forward along outgoing refs to the tail.
Cycle-safe and depth-capped. Makes long threads (e.g., a chapter
flowing across many designed pages) immediately legible — useful
in J5+ when threads start spanning more than two frames.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 16:09:44 -04:00
thePR0M3TH3ANandClaude Opus 4.7 4afe726704 feat(book): J3.b — click-to-link UX for text-frame threading
Wires the in/out ports added in J3.a. Workflow:

- Click an empty out port → enters 'loading' state (cursor turns to
  copy, body gets .book-port-loading, source port pulses, every
  empty in port highlights as a drop target).
- Click another frame's in port → links them. sourceLayer.nextFrame =
  { target, frameId }. Toast confirms.
- Click an already-linked out port → breaks the thread at that point.
- Click the same source out port again, or press Esc → cancels.
- A cycle guard refuses links that would close a loop (A → B → C,
  then trying C → A).

Pure data updates so far — no rendering change to the chapter
content. The pagination engine still uses the master-frame-clone
model from C4. J4 swaps in a thread-walking pagination engine that
honours these nextFrame refs.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 16:08:35 -04:00
thePR0M3TH3ANandClaude Opus 4.7 3cab9482e7 feat(book): J3.a — render in/out ports + frameId badge on text frames
First step of the threading UI. Every text-frame layer in book mode
now shows:

- An 'in' port (small square, top-left corner) — turns green when
  another frame's nextFrame ref points at this one.
- An 'out' port (small square, bottom-right corner) — turns green
  when this frame's nextFrame points somewhere.
- A tiny '⌗ <frameId>' badge in the bottom-left so the user can see
  the frame's stable id while authoring.

Reads thread-link state from layer.nextFrame on each frame (no
book.threads[] consumer yet — that lands in J4 with the pagination
refactor). Legacy text-frame layers (created pre-J2) get a frameId
backfilled the first time they render, so old books gain the new
ports automatically.

Click handling is not yet wired — J3.b adds the load-cursor + link
flow. The ports are visible affordances and tooltips today.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 16:01:07 -04:00
thePR0M3TH3ANandClaude Opus 4.7 2110e724b9 feat(book): J2 — first-class text-frame on designed pages + stable frameId
Generalises the text-frame layer from master-only (Phase C4) to any
book surface. Designed pages can now host text frames the same way
masters do, with the same '+ Text frame' button in the page-edit
bar.

What changed:

- addBookTextFrameToActiveMaster() → addBookTextFrame(targetOverride)
  that picks up book.editingMasterId OR book.editingEntryId depending
  on which surface is open. Legacy alias kept so old callers don't
  break.
- New layer carries a stable frameId (independent of layer.id) and
  a nextFrame field (null for now; J3 will use it for threading).
- bookMasterAddTextFrame button now visible in both master-edit
  AND page-edit modes. Title updated to read 'a text frame to this
  surface (master or designed page)'.
- Click handler now calls the generalized addBookTextFrame().

The frame still renders as the existing dashed-blue text-frame
shape with no content of its own — pagination still flows chapter
text through master frames as before. J3 wires the in/out ports
and the click-to-link UX; J4 makes the pagination engine walk
thread.frames[] instead of cloning masters.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 15:58:58 -04:00
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 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