Phase A4 skeleton: tiles are placeholders. Click a tile in Phase B to edit the page. Reflow / NIP-23 chapters arrive in Phase C.5; per-page Nostr publish in Phase D1.
+
+
+
Cover view ships in Phase A5. The cover designer (front + spine + back) will mount here, with the spine column derived from page count × paper thickness.
+
+
+
@@ -8060,6 +8143,139 @@
render();
}
+ // Stable opaque id for manifest entries. The id stays put across
+ // reorders, so anchor refs (`anchor.entryId`) survive shuffling
+ // the page list. Format mirrors Linked Designs' provisional ids.
+ function makeBookEntryId() {
+ return 'entry:' + Math.random().toString(36).slice(2, 10);
+ }
+ // Append a fresh designed-page entry to the manifest. Phase A4:
+ // entries carry ref:null until the page event is published in D1.
+ function addBookDesignedPage() {
+ const book = ensureBookState();
+ pushHistory('book: add page');
+ book.pages.push({
+ id: makeBookEntryId(),
+ type: 'design',
+ ref: null,
+ label: '',
+ masterRef: null,
+ skipNumber: false,
+ anchor: null,
+ gating: null,
+ });
+ renderBookPagesGrid();
+ syncInputs();
+ render();
+ }
+ function addBookTenPages() {
+ const book = ensureBookState();
+ pushHistory('book: add 10 pages');
+ for (let i = 0; i < 10; i++) {
+ book.pages.push({
+ id: makeBookEntryId(),
+ type: 'design', ref: null, label: '',
+ masterRef: null, skipNumber: false, anchor: null, gating: null,
+ });
+ }
+ renderBookPagesGrid();
+ syncInputs();
+ render();
+ }
+ function deleteBookPage(entryId) {
+ const book = state.book;
+ if (!book) return;
+ const idx = book.pages.findIndex(p => p.id === entryId);
+ if (idx < 0) return;
+ pushHistory('book: delete page');
+ book.pages.splice(idx, 1);
+ // Any other entry that anchored to this one drops back to null.
+ for (const p of book.pages) {
+ if (p.anchor && (p.anchor.entryId === entryId || p.anchor.after === entryId)) {
+ p.anchor = null;
+ }
+ }
+ renderBookPagesGrid();
+ syncInputs();
+ render();
+ }
+
+ // Render the Pages overview grid. Each tile is a placeholder
+ // (the actual rendered-page thumbnail lands in Phase B once the
+ // per-page canvas exists). The tile shows the page number from
+ // the manifest's numbering config, a label, and a delete affordance.
+ function renderBookPagesGrid() {
+ const grid = document.getElementById('bookPagesGrid');
+ if (!grid) return;
+ grid.innerHTML = '';
+ const book = state.book;
+ const pages = book?.pages || [];
+ const readout = document.getElementById('bookCountReadout');
+ if (readout) {
+ const reflowN = pages.filter(p => p.type === 'reflow').length;
+ readout.textContent = `${pages.length} page${pages.length === 1 ? '' : 's'} · ${reflowN} reflow`;
+ }
+ if (!pages.length) {
+ const empty = document.createElement('div');
+ empty.className = 'deckSlideEmpty';
+ empty.textContent = 'No pages yet. Click "+ Add page" to begin your book.';
+ grid.appendChild(empty);
+ return;
+ }
+ // Numbered-page counter walks the visible sequence, skipping
+ // any entry flagged skipNumber:true. The displayed number also
+ // honours pageNumbering.startAt + firstNumberedPage.
+ const numbering = book.spec?.pageNumbering;
+ let printedNum = numbering?.startAt ?? 1;
+ const firstNumberedIdx = (numbering?.firstNumberedPage ?? 1) - 1;
+ const fmt = numbering?.format || '{n}';
+ pages.forEach((p, idx) => {
+ const tile = document.createElement('div');
+ tile.className = `bookPageTile ${p.type === 'reflow' ? 'reflowSegment' : 'designedPage'}`;
+ tile.dataset.entryId = p.id;
+ const numEl = document.createElement('div');
+ numEl.className = 'bookPageTileNum';
+ if ((numbering?.enabled !== false) && !p.skipNumber && idx >= firstNumberedIdx) {
+ numEl.textContent = fmt.replace('{n}', String(printedNum));
+ printedNum++;
+ } else {
+ numEl.textContent = '—';
+ }
+ tile.appendChild(numEl);
+ const del = document.createElement('button');
+ del.type = 'button';
+ del.className = 'bookPageTileDelete';
+ del.title = 'Remove from book';
+ del.textContent = '×';
+ del.addEventListener('click', (e) => {
+ e.stopPropagation();
+ deleteBookPage(p.id);
+ });
+ tile.appendChild(del);
+ const label = document.createElement('div');
+ label.className = 'bookPageTileLabel';
+ if (p.type === 'reflow') {
+ label.textContent = p.chapterTitle || 'Chapter';
+ } else {
+ label.textContent = p.label || 'Designed page';
+ }
+ tile.appendChild(label);
+ grid.appendChild(tile);
+ });
+ }
+
+ function setBookSubView(view) {
+ const pages = document.getElementById('bookPagesView');
+ const cover = document.getElementById('bookCoverView');
+ const tPages = document.getElementById('bookViewTogglePages');
+ const tCover = document.getElementById('bookViewToggleCover');
+ const showCover = view === 'cover';
+ if (pages) pages.style.display = showCover ? 'none' : '';
+ if (cover) cover.style.display = showCover ? '' : 'none';
+ if (tPages) tPages.classList.toggle('active', !showCover);
+ if (tCover) tCover.classList.toggle('active', showCover);
+ }
+
function syncBookSidebarSummary() {
const el = $('bookSpecSummary');
if (!el) return;
@@ -12063,6 +12279,7 @@
syncDesignerInputs();
updateBodyClasses();
renderDeckSorter();
+ renderBookPagesGrid();
renderCreditsPanel();
if (state.templateMode === 'pixelart') ensureStandalonePixelEditor();
// After updateBodyClasses so the designer view is displayed/laid out —
@@ -30977,6 +31194,12 @@
on('publishConfirmCancelBtn', 'click', closePublishConfirmModal);
on('publishConfirmPublishBtn', 'click', handlePublishConfirmPublish);
+ // Book Designer — Pages overview + Cover view toggle.
+ on('bookAddPageBtn', 'click', addBookDesignedPage);
+ on('bookAddTenBtn', 'click', addBookTenPages);
+ on('bookViewTogglePages', 'click', () => setBookSubView('pages'));
+ on('bookViewToggleCover', 'click', () => setBookSubView('cover'));
+
// Book Designer — Document settings modal.
on('bookDocumentSettingsBtn', 'click', openBookDocumentSettings);
on('bookDocumentSettingsCloseBtn', 'click', closeBookDocumentSettings);