book(A2): Template-tab "Book" entry + mode switching

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.
This commit is contained in:
thePR0M3TH3AN committed 2026-06-02 14:14:32 -04:00
1 parent 314b582b0c
commit 7bed2a6a3f
1 file changed
+81 -2
+81 -2
View File
@@ -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 @@
<button type="button" class="layoutTab" id="mode-cover" data-template-mode="cover" role="tab">Case cover</button>
<button type="button" class="layoutTab" id="mode-disc" data-template-mode="disc" role="tab">Disc labels</button>
<button type="button" class="layoutTab" id="mode-deck" data-template-mode="deck" role="tab">Slide deck</button>
<button type="button" class="layoutTab" id="mode-book" data-template-mode="book" role="tab">Book</button>
</div>
</fieldset>
@@ -3196,6 +3211,17 @@
<p class="hint">Click a slide thumbnail to edit it. Drag thumbnails to reorder.</p>
</fieldset>
<fieldset class="workflowStep template-only" id="bookFieldset">
<legend data-step="2">Book</legend>
<p class="hint">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 <code>docs/BOOK_DESIGNER_FEATURE.md</code> for the full spec.</p>
<div id="bookSpecSummary" class="hint" style="margin-top:8px"></div>
<div class="buttonRow" style="margin-top:8px">
<button type="button" id="bookDocumentSettingsBtn" disabled>Document settings…</button>
<button type="button" id="bookManageMastersBtn" disabled>Manage masters…</button>
</div>
<p class="hint" style="margin-top:8px">Document settings, Pages overview, Cover view, and PDF export ship in subsequent phases. This entry is the Phase A2 skeleton.</p>
</fieldset>
<fieldset class="workflowStep template-only" id="deckThemeFieldset">
<legend data-step="3">Deck theme</legend>
<p class="hint">A deck theme restyles every slide in the thumbnails and Presenter Mode — the slides themselves are never changed.</p>
@@ -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 = ''
+ `<b>${s.bookSize || 'Custom'}</b> · ${fmt(s.trimW)} × ${fmt(s.trimH)} in`
+ `<br>Bleed ${fmt(s.bleed)} in · Paper ${fmt(s.paperThickness)} in/page`
+ `<br>Spine ${fmt(spineW)} in <span class="hint">(${spineNote})</span>`
+ `<br>${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));
}