From 55e60150f0a4aafb3f43392cc598b0ade27b5ba9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:54:04 -0400 Subject: [PATCH] book(E4): "Proof one page" quick export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 = -proof-.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 --- src/index.html | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/src/index.html b/src/index.html index 15d1572..ad0f7dd 100644 --- a/src/index.html +++ b/src/index.html @@ -4392,6 +4392,7 @@ + @@ -4568,6 +4569,7 @@ + @@ -9195,6 +9197,23 @@ render(); }); actions.appendChild(masterSel); + const proof = document.createElement('button'); + proof.type = 'button'; + proof.className = 'bookPageTileAction'; + proof.title = p.type === 'reflow' ? 'Proof chapter as PDF' : 'Proof page as PDF'; + proof.textContent = 'πŸ“„'; + proof.addEventListener('click', async (e) => { + e.stopPropagation(); + proof.disabled = true; + proof.textContent = '…'; + try { + await proofBookEntryPdf(p.id); + } finally { + proof.disabled = false; + proof.textContent = 'πŸ“„'; + } + }); + actions.appendChild(proof); const dup = document.createElement('button'); dup.type = 'button'; dup.className = 'bookPageTileAction'; @@ -25540,6 +25559,61 @@ } return { blob: interior, warnings }; } + // "Proof one page" / "Proof one chapter" quick export (E4). + // Skips the modal β€” one click β†’ PDF download scoped to a single + // manifest entry. Useful for sending a single page to a printer + // for colour / paper proofing before committing to a full run. + function _bookProofRangeForEntry(entryId) { + const seq = enumerateBookPhysicalPages(state.book); + let from = -1, to = -1; + for (let i = 0; i < seq.length; i++) { + if (seq[i].entryId !== entryId) continue; + if (from < 0) from = i; + to = i; + } + if (from < 0) return null; + return { from: from + 1, to: to + 1 }; + } + async function proofBookEntryPdf(entryId, opts = {}) { + const book = state.book; + if (!book) return; + const entry = book.pages.find(p => p.id === entryId); + if (!entry) return; + const range = _bookProofRangeForEntry(entryId); + if (!range) { toastError && toastError('Nothing to proof β€” entry has no physical pages.'); return; } + const slug = _bookExportDefaultSlug(); + const suffix = entry.type === 'reflow' + ? (entry.chapterTitle || 'chapter').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32) || 'chapter' + : (entry.label || `page-${range.from}`).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32) || `page-${range.from}`; + const filename = `${slug}-proof-${suffix}.pdf`; + const setStatus = opts.onStatus || (() => {}); + setStatus('Rendering proof…'); + try { + const result = await exportBookToPdf({ + range, + includeCover: false, + includeBleed: !!opts.includeBleed, + cropMarks: !!opts.cropMarks, + split: false, + }); + const a = document.createElement('a'); + const url = URL.createObjectURL(result.blob); + a.href = url; a.download = filename; + document.body.appendChild(a); a.click(); document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 4000); + const sizeKB = (result.blob.size / 1024).toFixed(0); + const w = result.warnings || []; + if (w.length) { + setStatus(`Proof exported (${sizeKB} KB) with ${w.length} warning${w.length === 1 ? '' : 's'}: ${w[0]}`, 'error'); + } else { + setStatus(`Proof exported (${sizeKB} KB).`, 'ok'); + } + } catch (err) { + console.warn('proof export failed', err); + setStatus('Proof export failed: ' + (err?.message || 'unknown'), 'error'); + } + } + function _bookExportDefaultSlug() { return (state.meta?.title || 'book') .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'book'; @@ -35047,6 +35121,21 @@ on('bookChapterEditorTitle', 'input', (e) => updateBookChapterTitle(e.target.value)); on('bookChapterEditorImportBtn', 'click', importBookChapterFromNostr); on('bookChapterEditorPublishBtn', 'click', publishBookChapterToNostr); + on('bookChapterEditorProofPdfBtn', 'click', async () => { + const book = state.book; + const id = book?.editingChapterId; + if (!id) return; + const msg = document.getElementById('bookChapterEditorSourceMsg'); + const setStatus = (text, kind) => { + if (!msg) return; + msg.textContent = text || ''; + msg.className = 'bookChapterEditorSourceMsg' + (kind ? ' ' + kind : ''); + }; + const btn = document.getElementById('bookChapterEditorProofPdfBtn'); + if (btn) btn.disabled = true; + try { await proofBookEntryPdf(id, { onStatus: setStatus }); } + finally { if (btn) btn.disabled = false; } + }); on('bookChapterEditorRefreshBtn', 'click', refreshBookChapterFromNostr); on('bookChapterEditorPinBtn', 'click', toggleBookChapterPin); on('bookChapterEditorUnlinkBtn', 'click', unlinkBookChapterFromNostr); @@ -35084,6 +35173,21 @@ on('bookPageEditNext', 'click', () => gotoBookPageRelative(1)); on('bookMasterAddTextFrame', 'click', () => addBookTextFrameToActiveMaster()); on('bookPageEditPublish', 'click', publishBookSurface); + on('bookPageEditProofPdf', 'click', async () => { + const book = state.book; + const entryId = book?.editingEntryId; + if (!entryId) return; + const status = document.getElementById('bookPageEditPublishStatus'); + const setStatus = (text, kind) => { + if (!status) return; + status.textContent = text || ''; + status.style.color = kind === 'error' ? '#b91c1c' : kind === 'ok' ? '#15803d' : 'var(--muted)'; + }; + const btn = document.getElementById('bookPageEditProofPdf'); + if (btn) btn.disabled = true; + try { await proofBookEntryPdf(entryId, { onStatus: setStatus }); } + finally { if (btn) btn.disabled = false; } + }); // Book Designer β€” Masters modal. on('bookManageMastersBtn', 'click', openBookMastersModal);