diff --git a/src/index.html b/src/index.html
index 7edbab0..9e8cf5e 100644
--- a/src/index.html
+++ b/src/index.html
@@ -4444,6 +4444,7 @@
+
@@ -8852,6 +8853,79 @@
return true;
}
+ // Import a single page from another book (Phase F3). Accepts a
+ // naddr / nevent / note / hex pointing at a casewrap-page event
+ // and creates a design entry whose `ref` points to it. The
+ // resolver (D1.5) paints the page's layers under the new entry's
+ // target prefix.
+ async function importBookPageFromNostr() {
+ const book = ensureBookState();
+ const raw = (typeof prompt === 'function')
+ ? prompt('Paste an naddr / nevent / note / hex pointing at a casewrap-page event:')
+ : '';
+ if (!raw) return;
+ const decoded = decodeNostrId(raw.trim());
+ if (!decoded) { toastError && toastError('Could not decode that id.'); return; }
+ const decodedForResolver = decoded.type === 'naddr'
+ ? decoded
+ : { type: 'nevent', eventId: decoded.eventId, relays: decoded.relays || [], author: decoded.author || '', kind: decoded.kind || null };
+ try {
+ const ev = await _resolveEmbedEvent(decodedForResolver);
+ if (!ev) { toastError && toastError('No event found on your relays.'); return; }
+ if (ev.kind !== 30078) { toastError && toastError(`Event is kind ${ev.kind}, not a casewrap-page.`); return; }
+ // Validate it's a page event (role tag preferred; templateMode in content as fallback).
+ const roleTag = (ev.tags || []).find(t => t[0] === 'role')?.[1] || '';
+ let payload = null;
+ try {
+ const decrypted = await _decryptBookSidecarEvent(ev);
+ if (decrypted) payload = decrypted;
+ } catch {}
+ if (!payload) {
+ try { payload = JSON.parse(ev.content || ''); } catch {}
+ }
+ const role = roleTag || payload?.page?.role || '';
+ if (role && role !== 'interior') {
+ const ok = confirm(`This event's role is "${role}", not "interior". Add as a page anyway?`);
+ if (!ok) return;
+ }
+ const dTag = (ev.tags || []).find(t => t[0] === 'd' && t[1])?.[1] || (decoded.dTag || '');
+ if (!dTag || !ev.pubkey) { toastError && toastError('Event is missing d-tag / pubkey.'); return; }
+ // Bail if already in this manifest.
+ const already = (book.pages || []).some(p => p.ref?.pubkey === ev.pubkey && p.ref?.dTag === dTag);
+ if (already) { toastError && toastError('That page is already in this book.'); return; }
+ pushHistory('book: import page');
+ const entryId = makeBookEntryId();
+ const newEntry = {
+ id: entryId,
+ type: 'design',
+ ref: {
+ kind: 30078, pubkey: ev.pubkey, dTag,
+ relays: (decoded.relays || []).slice(0, 4),
+ encrypted: (ev.tags || []).some(t => t[0] === 'encrypted') || undefined,
+ },
+ label: (ev.tags || []).find(t => t[0] === 'title' && t[1])?.[1] || '',
+ masterRef: null,
+ masterId: null,
+ skipNumber: false,
+ anchor: null,
+ gating: null,
+ };
+ book.pages.push(newEntry);
+ // Seed the cache + apply the resolved layers immediately so
+ // the tile renders without waiting on a second relay round-trip.
+ _bookCacheWrite(newEntry.ref, ev);
+ if (payload?.page) {
+ _applyResolvedBookPage(entryId, payload);
+ }
+ syncInputs();
+ render();
+ if (typeof toastSuccess === 'function') toastSuccess('Page imported.');
+ } catch (err) {
+ console.warn('book: importBookPageFromNostr failed', err);
+ toastError && toastError('Import failed: ' + (err?.message || 'unknown error'));
+ }
+ }
+
// Starter book templates (Phase F1). Each preset seeds the spec
// + a default master with a text-frame + a starter chapter so the
// user lands on a working book instead of an empty pages overview.
@@ -35338,6 +35412,7 @@
on('bookAddPageBtn', 'click', addBookDesignedPage);
on('bookAddTenBtn', 'click', addBookTenPages);
on('bookAddChapterBtn', 'click', addBookChapter);
+ on('bookImportPageBtn', 'click', importBookPageFromNostr);
// Chapter editor panes.
on('bookChapterEditorBack', 'click', closeBookChapter);
on('bookChapterEditorTextarea', 'input', (e) => updateBookChapterMarkdown(e.target.value));