From 461ab8572866a99f40351faabfe9dcce3c034515 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:59:36 -0400 Subject: [PATCH] =?UTF-8?q?book(F2):=20deck=20=E2=86=92=20book=20conversio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new "→ Convert to book" action lives in two places: - Deck-sorter toolbar (next to "+ Add slide") — for users already in deck mode who want to flip their existing slides into a book. - Book starter panel's "Or import from a deck" block — surfaces automatically when the user enters book mode with no pages and a non-empty deck. How the conversion works: - convertDeckToBook({ presetKey }) resets the book to a starter spec (US Trade 6×9 by default; can pass any BOOK_SIZE_PRESETS key) with a default master. - Each slide becomes one designed-page entry. Slide layers letterbox-fit into the trim: scale = min(trimW / slideWIn, trimH / slideHIn) layers reposition to offset + x*scale, offset + y*scale layers' w/h scale linearly; fontSize and stroke.width scale too - Slide background (when non-default) carries to entry.background. - Slide.name carries to the entry's label. - Confirms before clobbering an existing book. - Switches templateMode to 'book' on success + toasts a "Imported N slides as designed pages." message. Co-Authored-By: Claude Opus 4.7 --- src/index.html | 104 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/src/index.html b/src/index.html index dbacbf4..7edbab0 100644 --- a/src/index.html +++ b/src/index.html @@ -4400,6 +4400,7 @@
0 slides
+
@@ -8768,6 +8769,89 @@ // label. Layers for the master live in state.layers under // `book-master:`. The first master created becomes the // default automatically. + // Convert the current deck into a book (Phase F2). Each slide + // becomes a designed page; layers letterbox-fit into the trim + // (contain-scaled + centered). Switches templateMode to 'book'. + function convertDeckToBook({ presetKey } = {}) { + const deck = state.deck; + const slides = deck?.slides; + if (!Array.isArray(slides) || !slides.length) { + toastError && toastError('No slides to convert — add a slide first.'); + return false; + } + const existing = state.book?.pages?.length || 0; + if (existing) { + const ok = confirm(`This book already has ${existing} entr${existing === 1 ? 'y' : 'ies'}. Continue and replace it with ${slides.length} pages from the deck?`); + if (!ok) return false; + } + pushHistory('book: convert from deck'); + const book = ensureBookState(); + const dims = BOOK_SIZE_PRESETS[presetKey] || BOOK_SIZE_PRESETS['US Trade 6×9']; + const fresh = defaultBookSpec(); + book.spec = { ...fresh, trimW: dims.trimW, trimH: dims.trimH, bookSize: presetKey || 'US Trade 6×9' }; + book.cover = null; + book.masters = []; + book.pages = []; + state.layers = (state.layers || []).filter(l => !String(l.target || '').startsWith('book-')); + // Default master (no text-frame — designed pages don't need one). + const master = { id: makeBookMasterId(), label: 'Body pages' }; + book.masters.push(master); + book.spec.defaultMasterId = master.id; + const trimW = Number(book.spec.trimW), trimH = Number(book.spec.trimH); + const bleed = Number(book.spec.bleed) || 0; + let nextZ = 1; + for (const slide of slides) { + const slideWIn = ((slide.slide?.width || 1920) / 96); + const slideHIn = ((slide.slide?.height || 1080) / 96); + const scale = Math.min(trimW / slideWIn, trimH / slideHIn); + const fitW = slideWIn * scale; + const fitH = slideHIn * scale; + const offX = bleed + (trimW - fitW) / 2; + const offY = bleed + (trimH - fitH) / 2; + const entryId = makeBookEntryId(); + book.pages.push({ + id: entryId, + type: 'design', + ref: null, + label: slide.name || '', + masterRef: null, + masterId: null, + skipNumber: false, + anchor: null, + gating: null, + background: slide.slide?.background || '', + }); + const target = `book-page:${entryId}`; + const slideLayers = Array.isArray(slide.layers) ? slide.layers : []; + for (const src of slideLayers) { + const cloned = { ...src }; + cloned.id = makeLayerId(); + cloned.target = target; + cloned.x = offX + (Number(src.x) || 0) * scale; + cloned.y = offY + (Number(src.y) || 0) * scale; + cloned.w = Math.max(0.01, (Number(src.w) || 0) * scale); + cloned.h = Math.max(0.01, (Number(src.h) || 0) * scale); + if (typeof src.fontSize === 'number') cloned.fontSize = src.fontSize * scale; + if (src.stroke && typeof src.stroke === 'object') { + cloned.stroke = { ...src.stroke }; + if (typeof src.stroke.width === 'number') cloned.stroke.width = src.stroke.width * scale; + } + cloned.z = nextZ++; + state.layers.push(cloned); + } + } + // Flip into book mode and reset publish state. + state.templateMode = 'book'; + if (state.book) state.book.editingEntryId = null; + book.lastPublishedManifestSerial = null; + syncInputs(); + render(); + if (typeof toastSuccess === 'function') { + toastSuccess(`Imported ${slides.length} slide${slides.length === 1 ? '' : 's'} as designed pages.`); + } + return true; + } + // 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. @@ -9161,6 +9245,18 @@ { key: 'A4 8.27×11.69', label: 'A4', dims: '8.27 × 11.69 in', desc: 'Letter-size manual / textbook' }, { key: 'Square 8×8', label: 'Square', dims: '8 × 8 in', desc: "Children's book, art book" }, ]; + const deckSlides = state.deck?.slides?.length || 0; + const deckBlock = deckSlides ? ` +
+

Or import from a deck

+

You have ${deckSlides} slide${deckSlides === 1 ? '' : 's'} in the current deck. Convert each slide into a designed page (letterbox-fit, centered).

+ +
+ ` : ''; starter.innerHTML = `

Start from a template

Pick a size and we'll create a starter book with a default master + a sample chapter ready to edit.

@@ -9173,15 +9269,20 @@ `).join('')} + ${deckBlock}

Or skip the template — click + Add page, + Add chapter, or Document settings… to start from scratch.

`; grid.appendChild(starter); - starter.querySelectorAll('.bookStarterCard').forEach(btn => { + starter.querySelectorAll('.bookStarterCard[data-preset]').forEach(btn => { btn.addEventListener('click', () => { const preset = btn.dataset.preset; applyBookStarterTemplate(preset); }); }); + const importDeckBtn = starter.querySelector('#bookStarterImportDeckBtn'); + if (importDeckBtn) { + importDeckBtn.addEventListener('click', () => convertDeckToBook()); + } return; } // Numbered-page counter walks the visible sequence, skipping @@ -33412,6 +33513,7 @@ // ---- Deck Builder wiring ---- $('deckAddSlideBtn')?.addEventListener('click', deckAddBlankSlide); $('deckSorterAddBtn')?.addEventListener('click', deckAddBlankSlide); + $('deckToBookBtn')?.addEventListener('click', () => convertDeckToBook()); $('deckImportSlideBtn')?.addEventListener('click', openDeckSlideImportBrowser); $('deckImportFileBtn')?.addEventListener('click', deckImportSlideFromFile); $('importPptxBtn')?.addEventListener('click', onImportPptxClick);