From 7bed2a6a3fd19958f1aabb8b383e56dea94b68a3 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:14:32 -0400 Subject: [PATCH] book(A2): Template-tab "Book" entry + mode switching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the Book button to the Template tab's layout picker (alongside Case cover / Disc labels / Slide deck) and wires the applyLayout('book') path so the user can switch into book mode. - `mode-book` button + click binding via the existing applyLayout loop (one-line list addition keeps every layout tab routed through the same handler). - `applyLayout('book')` calls `ensureBookState()` to lazy-init the manifest the first time a user enters the mode. - New `template-book` body class + CSS gating: the new `#bookFieldset` sidebar block shows only in book mode, and the cover / disc / deck / custom-art fieldsets hide in book mode so the sidebar stays focused on the spec + masters affordances. - `WORKFLOW_ORDER` learns the book column. - `sheetW` / `sheetH` fall back to trim + bleed when `templateMode === 'book'` so the canvas-host placeholder is sized correctly (it stays blank in this phase; the actual Pages overview + Cover view come in A4/A5). - Sidebar fieldset shows a derived spec summary (trim, bleed, paper thickness, derived spine width, page + master count) rendered by a new `syncBookSidebarSummary()` hook into `syncInputs()`. Book-state helpers (`bookPageCount`, `bookSpineWidthInches`) are pure functions so future phases can reuse them. - Two placeholder buttons (Document settings… / Manage masters…) are stubbed disabled with a hint pointing at the upcoming phases, so users can see where the controls will land. --- src/index.html | 83 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/src/index.html b/src/index.html index 23431b7..3d2c3b4 100644 --- a/src/index.html +++ b/src/index.html @@ -500,6 +500,20 @@ body.template-deck #deckThemeFieldset { display: block; } #deckSlideThemeWrap { display: none; margin-top: 10px; } body.editing-deck-slide #deckSlideThemeWrap { display: block; } + /* Book Designer sidebar fieldset shows only in book mode. */ + #bookFieldset { display: none; } + body.template-book #bookFieldset { display: block; } + /* Hide cover / disc / artwork panels that aren't relevant in book + mode (Phase A2 — book mode shows only its own fieldset + layers + + metadata + save). The Pages overview canvas-host comes in + Phase A4; until then the canvas-host remains blank. */ + body.template-book #coverSizeFieldset, + body.template-book #coverArtworkFieldset, + body.template-book #discArtworkFieldset, + body.template-book #jewelArtworkFieldset, + body.template-book #customArtFieldset, + body.template-book #deckFieldset, + body.template-book #deckThemeFieldset { display: none !important; } #layersFieldset { order: 40; } #designerMetadataFieldset { order: 45; } #exportFieldset { order: 50; } @@ -3013,6 +3027,7 @@ + @@ -3196,6 +3211,17 @@

Click a slide thumbnail to edit it. Drag thumbnails to reorder.

+
+ Book +

A book combines a cover spread (front + spine + back), an ordered list of designed pages, optional reflowable chapters written as NIP-23 markdown, and master pages for shared running heads and page numbers. See docs/BOOK_DESIGNER_FEATURE.md for the full spec.

+
+
+ + +
+

Document settings, Pages overview, Cover view, and PDF export ship in subsequent phases. This entry is the Phase A2 skeleton.

+
+
Deck theme

A deck theme restyles every slide in the thumbnails and Presenter Mode — the slides themselves are never changed.

@@ -7810,6 +7836,38 @@ if (!state.book) state.book = defaultBookState(); return state.book; } + // Page count + derived spine width readouts for the sidebar. + // Page count is the manifest entry count for Phase A — once reflow + // segments land in C.5 this counts physical pages after pagination. + function bookPageCount() { + return (state.book?.pages || []).length; + } + function bookSpineWidthInches() { + const spec = state.book?.spec; + if (!spec) return 0; + if (!spec.spineFromPages) return Number(spec.spineWManual) || 0; + return bookPageCount() * (Number(spec.paperThickness) || 0); + } + function syncBookSidebarSummary() { + const el = $('bookSpecSummary'); + if (!el) return; + if (state.templateMode !== 'book' || !state.book) { + el.textContent = ''; + return; + } + const s = state.book.spec; + const pageCount = bookPageCount(); + const spineW = bookSpineWidthInches(); + const spineNote = s.spineFromPages + ? `derived from ${pageCount} page${pageCount === 1 ? '' : 's'}` + : 'manual'; + const fmt = (n) => Number(n).toFixed(n % 1 === 0 ? 0 : 3); + 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'}`; + } function isLayerForCurrentMode(layer) { const t = String(layer?.target || ''); @@ -8857,6 +8915,10 @@ if (state.templateMode === 'disc') return discTemplate.sheetW; if (state.templateMode === 'jewel') return jewelTemplate.sheetW; if (isCanvasMode(state.templateMode)) return customArtCanvasInches().wIn; + if (state.templateMode === 'book') { + const s = state.book?.spec; + return (s ? (Number(s.trimW) || 0) + (Number(s.bleed) || 0) * 2 : 6); + } return totalTrimW() + state.bleed * 2; } @@ -8864,6 +8926,13 @@ if (state.templateMode === 'disc') return discTemplate.sheetH; if (state.templateMode === 'jewel') return jewelTemplate.sheetH; if (isCanvasMode(state.templateMode)) return customArtCanvasInches().hIn; + if (state.templateMode === 'book') { + // Phase A2: the canvas-host renders blank in book mode (no + // active page surface yet). Use trim + bleed so the host has + // sensible dimensions for the placeholder. + const s = state.book?.spec; + return (s ? (Number(s.trimH) || 0) + (Number(s.bleed) || 0) * 2 : 9); + } return state.trimH + state.bleed * 2; } @@ -8925,12 +8994,13 @@ cover: !isDesignerApp && state.templateMode === 'cover', disc: !isDesignerApp && state.templateMode === 'disc', deck: !isDesignerApp && state.templateMode === 'deck', + book: !isDesignerApp && state.templateMode === 'book', customart: !isDesignerApp && state.templateMode === 'customart', slide: !isDesignerApp && state.templateMode === 'slide', pixelart: !isDesignerApp && state.templateMode === 'pixelart', discdesign: isDesignerApp }; - for (const m of ['cover', 'disc', 'deck', 'customart', 'slide', 'pixelart', 'discdesign']) { + for (const m of ['cover', 'disc', 'deck', 'book', 'customart', 'slide', 'pixelart', 'discdesign']) { const button = $(`mode-${m}`); if (!button) continue; button.classList.toggle('active', !!layoutActive[m]); @@ -9050,6 +9120,7 @@ customart: ['designerLayoutFieldset', 'customArtFieldset', 'layersFieldset', 'metadataFieldset', 'exportFieldset', 'printSaveFieldset'], slide: ['designerLayoutFieldset', 'customArtFieldset', 'layersFieldset', 'metadataFieldset', 'exportFieldset', 'printSaveFieldset'], deck: ['layoutFieldset', 'deckFieldset', 'deckThemeFieldset', 'metadataFieldset', 'printSaveFieldset'], + book: ['layoutFieldset', 'bookFieldset', 'metadataFieldset', 'printSaveFieldset'], pixelart: ['designerLayoutFieldset', 'metadataFieldset', 'printSaveFieldset'], designer: ['designerLayoutFieldset', 'designerFieldset', 'layersFieldset', 'designerMetadataFieldset', 'designerPrintSaveFieldset'] }; @@ -9078,6 +9149,7 @@ document.body.classList.toggle('template-pixelart', state.templateMode === 'pixelart'); document.body.classList.toggle('template-slide', state.templateMode === 'slide'); document.body.classList.toggle('template-deck', state.templateMode === 'deck'); + document.body.classList.toggle('template-book', state.templateMode === 'book'); document.body.classList.toggle('editing-deck-slide', !!state.deck && state.deck.editingIndex !== null); document.body.classList.toggle('hide-guides', !state.showGuides); document.body.classList.toggle('hide-bleed', !state.showBleed); @@ -9089,6 +9161,7 @@ document.body.classList.toggle('app-template', state.appMode !== 'designer'); updateLayerTargetOptions(); renumberWorkflowSteps(); + syncBookSidebarSummary(); } function renderDimensions() { @@ -28588,6 +28661,12 @@ state.deck = state.deck || { theme: { fontFamily: '', background: '' }, slides: [], editingIndex: null }; state.deck.editingIndex = null; } + if (layout === 'book') { + // Lazy-init the book block when first entering Book mode. The + // factory lives next to the other state defaults so the shape + // matches docs/BOOK_DESIGNER_FEATURE.md. + ensureBookState(); + } if (isCanvasMode(layout) || layout === 'pixelart') state._lastDesignerLayout = layout; else state._lastTemplateLayout = layout; syncInputs(); @@ -28839,7 +28918,7 @@ function attachEvents() { // Layout tabs: cover/disc live on the Template tab; customart/discdesign // on the Designer tab. applyLayout() sorts out the appMode crossing. - for (const layout of ['cover', 'disc', 'deck', 'customart', 'slide', 'pixelart', 'discdesign']) { + for (const layout of ['cover', 'disc', 'deck', 'book', 'customart', 'slide', 'pixelart', 'discdesign']) { const btn = $(`mode-${layout}`); if (btn) btn.addEventListener('click', () => applyLayout(layout)); }