From c329eb4839d21313c85cfa3d7acd21d6aeb026d0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 3 Jun 2026 08:45:45 -0400 Subject: [PATCH] book(D1.2): publish page + master as casewrap-page events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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:` or `book-master:`, 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 --- src/index.html | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/index.html b/src/index.html index 8fecdab..e842077 100644 --- a/src/index.html +++ b/src/index.html @@ -4536,6 +4536,8 @@ + + @@ -9878,6 +9880,147 @@ if (pinBtn) pinBtn.disabled = false; } } + // Build the kind-30078 payload for a designed book page. Layers + // are filtered to just this entry and the target prefix is dropped + // so the event renders standalone (Linked Designs / embed). + function bookPagePayload(entry, role = 'interior') { + const spec = state.book?.spec || {}; + const trimW = Number(spec.trimW) || 6; + const trimH = Number(spec.trimH) || 9; + const bleed = Number(spec.bleed) || 0; + const isMaster = role === 'master'; + const target = isMaster ? `book-master:${entry.id}` : `book-page:${entry.id}`; + const flatTarget = isMaster ? 'book-master' : 'book-page'; + const pageLayers = (state.layers || []) + .filter(l => l.target === target) + .map(l => ({ ...l, target: flatTarget })); + return { + version: typeof SCHEMA_VERSION === 'number' ? SCHEMA_VERSION : 5, + templateMode: 'book-page', + templateType: 'book-page', + meta: { + ...(state.meta || {}), + title: (isMaster ? (entry.label || 'Master') : (entry.label || state.meta?.title || 'Page')), + }, + page: { + role, + trimW, trimH, bleed, + coverSpread: null, + background: entry.background || '', + layers: pageLayers, + }, + }; + } + // Publish a designed page as a casewrap-page event. Stable d-tag + // built from the entry id so re-publishes replace in place. + async function publishBookPageToNostr() { + const book = state.book; + const entryId = book?.editingEntryId; + if (!entryId) return; + const entry = book.pages.find(p => p.id === entryId); + if (!entry || entry.type !== 'design') return; + const status = document.getElementById('bookPageEditPublishStatus'); + const setMsg = (text, kind) => { + if (!status) return; + status.textContent = text || ''; + status.style.color = kind === 'error' ? '#b91c1c' + : kind === 'ok' ? '#15803d' + : 'var(--muted)'; + }; + if (!window.nostr) { setMsg('Sign in with a Nostr extension first.', 'error'); return; } + const target = `book-page:${entry.id}`; + const hasLayers = (state.layers || []).some(l => l.target === target); + if (!hasLayers && !(entry.background && entry.background.toLowerCase() !== '#ffffff')) { + setMsg('Page is empty — nothing to publish.', 'error'); + return; + } + const payload = bookPagePayload(entry, 'interior'); + const dTag = `book-page:${entry.id}`; + const now = Math.floor(Date.now() / 1000); + const tags = [ + ['d', dTag], + ['t', 'casewrap'], + ['t', 'casewrap-page'], + ['title', payload.meta?.title || 'Page'], + ['client', 'Artstr Studio'], + ['role', 'interior'], + ]; + const evt = { kind: 30078, created_at: now, tags, content: JSON.stringify(payload) }; + const btn = document.getElementById('bookPageEditPublish'); + if (btn) btn.disabled = true; + setMsg('Signing + publishing…'); + try { + const signed = await publishCommunityEvent(evt); + const relays = splitRelayList(state.community?.relays || '').slice(0, 2); + pushHistory('book: page publish'); + entry.ref = { kind: 30078, pubkey: signed.pubkey, dTag, relays }; + setMsg(`Published ${signed.id.slice(0, 8)}…`, 'ok'); + render(); + } catch (err) { + console.warn('page publish failed', err); + setMsg('Publish failed: ' + (err?.message || 'unknown error'), 'error'); + } finally { + if (btn) btn.disabled = false; + } + } + // Publish the currently-edited master as a casewrap-page event + // with role:'master'. Mirrors page publish; d-tag uses the master + // id so re-publishes replace in place. + async function publishBookMasterToNostr() { + const book = state.book; + const masterId = book?.editingMasterId; + if (!masterId) return; + const m = (book.masters || []).find(x => x.id === masterId); + if (!m) return; + const status = document.getElementById('bookPageEditPublishStatus'); + const setMsg = (text, kind) => { + if (!status) return; + status.textContent = text || ''; + status.style.color = kind === 'error' ? '#b91c1c' + : kind === 'ok' ? '#15803d' + : 'var(--muted)'; + }; + if (!window.nostr) { setMsg('Sign in with a Nostr extension first.', 'error'); return; } + const target = `book-master:${m.id}`; + const hasLayers = (state.layers || []).some(l => l.target === target); + if (!hasLayers) { setMsg('Master has no layers — nothing to publish.', 'error'); return; } + const payload = bookPagePayload(m, 'master'); + const dTag = `book-master:${m.id}`; + const now = Math.floor(Date.now() / 1000); + const tags = [ + ['d', dTag], + ['t', 'casewrap'], + ['t', 'casewrap-page'], + ['title', m.label || 'Master'], + ['client', 'Artstr Studio'], + ['role', 'master'], + ]; + const evt = { kind: 30078, created_at: now, tags, content: JSON.stringify(payload) }; + const btn = document.getElementById('bookPageEditPublish'); + if (btn) btn.disabled = true; + setMsg('Signing + publishing…'); + try { + const signed = await publishCommunityEvent(evt); + const relays = splitRelayList(state.community?.relays || '').slice(0, 2); + pushHistory('book: master publish'); + m.ref = { kind: 30078, pubkey: signed.pubkey, dTag, relays }; + setMsg(`Published ${signed.id.slice(0, 8)}…`, 'ok'); + render(); + } catch (err) { + console.warn('master publish failed', err); + setMsg('Publish failed: ' + (err?.message || 'unknown error'), 'error'); + } finally { + if (btn) btn.disabled = false; + } + } + // Dispatch the Publish button to the right helper based on which + // book-mode view is active (page edit vs master edit). + function publishBookSurface() { + const book = state.book; + if (book?.editingMasterId) return publishBookMasterToNostr(); + if (book?.editingEntryId) return publishBookPageToNostr(); + } + // Build a NIP-23 d-tag slug from the chapter title (or entry id). // Kept lowercase alphanumeric + hyphens so it round-trips through // relay query strings cleanly. @@ -10092,6 +10235,9 @@ bar.style.display = (editingPage || editingMaster) ? '' : 'none'; if (!editingPage && !editingMaster) return; const tfBtn = document.getElementById('bookMasterAddTextFrame'); + const pubBtn = document.getElementById('bookPageEditPublish'); + const pubStatus = document.getElementById('bookPageEditPublishStatus'); + if (pubStatus) pubStatus.textContent = ''; if (editingMaster) { const m = book.masters.find(x => x.id === book.editingMasterId); if (label) label.textContent = `Master: ${m?.label || 'Untitled'}`; @@ -10099,6 +10245,12 @@ if (prev) prev.style.display = 'none'; if (next) next.style.display = 'none'; if (tfBtn) tfBtn.style.display = ''; + if (pubBtn) { + pubBtn.textContent = m?.ref ? 'Re-publish master' : 'Publish master'; + pubBtn.title = m?.ref + ? `Replace ${m.ref.dTag} on relays` + : 'Publish this master as a casewrap-page event (role:"master")'; + } return; } // editingPage path @@ -10106,6 +10258,13 @@ if (next) next.style.display = ''; if (tfBtn) tfBtn.style.display = 'none'; if (back) back.textContent = '← Back to Pages'; + if (pubBtn) { + const entry = book.pages.find(p => p.id === book.editingEntryId); + pubBtn.textContent = entry?.ref ? 'Re-publish page' : 'Publish page'; + pubBtn.title = entry?.ref + ? `Replace ${entry.ref.dTag} on relays` + : 'Publish this page as a casewrap-page event'; + } const designed = book.pages.filter(p => p.type === 'design'); const idx = designed.findIndex(p => p.id === book.editingEntryId); const total = designed.length; @@ -33298,6 +33457,7 @@ on('bookPageEditPrev', 'click', () => gotoBookPageRelative(-1)); on('bookPageEditNext', 'click', () => gotoBookPageRelative(1)); on('bookMasterAddTextFrame', 'click', () => addBookTextFrameToActiveMaster()); + on('bookPageEditPublish', 'click', publishBookSurface); // Book Designer — Masters modal. on('bookManageMastersBtn', 'click', openBookMastersModal);