diff --git a/src/index.html b/src/index.html index 517380f..5697578 100644 --- a/src/index.html +++ b/src/index.html @@ -10531,6 +10531,36 @@ return summary; } + // Manifest dirty tracking. After a successful manifest publish + // we snapshot the serialised payload onto state.book.lastPublishedManifestSerial; + // isBookManifestDirty() compares the current serialisation to + // that snapshot. Used by the sidebar hint so the user knows when + // the manifest needs another publish. + function isBookManifestDirty() { + const book = state.book; + if (!book) return false; + const snapshot = book.lastPublishedManifestSerial; + if (!snapshot) { + // No snapshot but we have refs → counted as dirty so the + // sidebar prods the user to publish for the first time. + const hasAnyRef = !!book.cover?.ref + || (book.masters || []).some(m => m.ref) + || (book.pages || []).some(p => p.ref || p.textRef); + return hasAnyRef; + } + try { + const current = JSON.stringify(bookManifestPayload()); + return current !== snapshot; + } catch { + return false; + } + } + function markBookManifestClean() { + const book = state.book; + if (!book) return; + try { book.lastPublishedManifestSerial = JSON.stringify(bookManifestPayload()); } catch {} + } + // Build the casewrap-book manifest payload. Lean — references // sibling page / master / cover / chapter events via {kind, pubkey, // dTag} coords instead of inlining their layers. Phase D1 hands @@ -11157,11 +11187,24 @@ ? `derived from ${pageCount} page${pageCount === 1 ? '' : 's'}` : 'manual'; const fmt = (n) => Number(n).toFixed(n % 1 === 0 ? 0 : 3); + // Manifest dirty hint — sidecars publish independently, so the + // manifest can be "out of sync" with the per-page events. Tell + // the user explicitly so they don't think Publish-page was the + // last step. + let dirtyHint = ''; + try { + if (isBookManifestDirty()) { + dirtyHint = '
⚠ Manifest has unsaved changes — Publish to update the casewrap-book event.'; + } else if (state.book.lastPublishedManifestSerial) { + dirtyHint = '
✓ Manifest is in sync with the published casewrap-book event.'; + } + } catch {} el.innerHTML = '' + `${s.bookSize || 'Custom'} · ${fmt(s.trimW)} × ${fmt(s.trimH)} in` + `
Bleed ${fmt(s.bleed)} in · Paper ${fmt(s.paperThickness)} in/page` + `
Spine ${fmt(spineW)} in (${spineNote})` - + `
${pageCount} page${pageCount === 1 ? '' : 's'} · ${(state.book.masters || []).length} master${(state.book.masters || []).length === 1 ? '' : 's'}`; + + `
${pageCount} page${pageCount === 1 ? '' : 's'} · ${(state.book.masters || []).length} master${(state.book.masters || []).length === 1 ? '' : 's'}` + + dirtyHint; syncBookVisibilityChip(); } // Sync the Public/Private chip in the book sidebar from @@ -21648,6 +21691,9 @@ const signed = await publishEncryptedDesign(payload); const label = isEditing() ? `Updated "${state.meta.title}" (private)` : `Private "${state.meta.title}"`; await finalizePublishedTemplateEvent(signed, { label }); + if (state.templateMode === 'book' && typeof markBookManifestClean === 'function') { + try { markBookManifestClean(); } catch {} + } if (isEditing()) exitEditMode(); return signed; } @@ -21677,6 +21723,12 @@ const signed = await publishCommunityEvent(evt); const label = isEditing() ? `Updated "${state.meta.title}"` : `Template "${state.meta.title}"`; await finalizePublishedTemplateEvent(signed, { label }); + // Book manifest: snapshot the published serialisation so the + // sidebar's dirty hint flips to "Saved" until the user makes + // another manifest-affecting change. + if (state.templateMode === 'book' && typeof markBookManifestClean === 'function') { + try { markBookManifestClean(); } catch {} + } if (isEditing()) exitEditMode(); return signed; } @@ -29098,6 +29150,25 @@ else if (row.mode === 'customart' || row.mode === 'slide') renderCustomArtPreviewDOM(previewWrap, payload, { compact: true }); else if (row.mode === 'deck') renderDeckPreviewDOM(previewWrap, payload, { compact: true }); else if (row.mode === 'pixelart') renderPixelArtPreviewDOM(previewWrap, payload, { compact: true }); + else if (row.mode === 'book') { + // Book manifests don't ship layer data — render a + // chapbook-shaped card with title + entry counts so the + // feed has something to display instead of "Preview + // unavailable". + const spec = payload.book?.spec || {}; + const pages = payload.book?.pages || []; + const designed = pages.filter(p => p.type === 'design').length; + const reflow = pages.filter(p => p.type === 'reflow').length; + const trimLabel = spec.bookSize || `${spec.trimW || 6} × ${spec.trimH || 9} in`; + previewWrap.style.cssText = 'width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:14px;box-sizing:border-box;background:linear-gradient(135deg,#fef3c7 0%,#fde68a 50%,#f59e0b 100%)'; + previewWrap.innerHTML = ` +
+
${escapeHtml(payload.meta?.title || 'Untitled book')}
+
${escapeHtml(trimLabel)}
+
${designed} page${designed === 1 ? '' : 's'}${reflow ? ` · ${reflow} chapter${reflow === 1 ? '' : 's'}` : ''}
+
+ `; + } else renderTemplatePreviewDOM(previewWrap, payload, { compact: true }); } catch {} } @@ -29865,7 +29936,7 @@ return btn; } - const MODE_LABELS = { cover: 'Case cover', disc: 'Disc sheet', jewel: 'CD jewel inserts', 'disc-design': 'Disc design', customart: 'Custom art', slide: 'Slide', deck: 'Slide deck', pixelart: 'Pixel art' }; + const MODE_LABELS = { cover: 'Case cover', disc: 'Disc sheet', jewel: 'CD jewel inserts', 'disc-design': 'Disc design', customart: 'Custom art', slide: 'Slide', deck: 'Slide deck', pixelart: 'Pixel art', book: 'Book' }; function buildDiscSheetThumb(thumb, payload, compact = false) { const slots = payload?.discTemplate?.discs || payload?.discs || {};