diff --git a/src/index.html b/src/index.html
index 0de60d0..2bb22f6 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1581,6 +1581,15 @@
position: absolute; top: 6px; left: 8px;
font-size: .72rem; color: var(--muted); font-weight: 600;
}
+ /* Master layers shown as an underlay while editing a page —
+ click-through and slightly faded so the page layers above
+ are obviously the editable ones. Master-edit mode (the
+ master IS the editing target) keeps them opaque +
+ interactive via the :not(.editing-book-master) guard. */
+ body.editing-book-page:not(.editing-book-master) .freeLayer[data-target^="book-master:"] {
+ pointer-events: none;
+ opacity: 0.55;
+ }
.bookMasterRow {
display: flex; align-items: center; gap: 8px;
padding: 8px 10px; border: 1px solid var(--line);
@@ -8336,6 +8345,19 @@
render();
}
+ // Resolve a page's effective master — its own `masterId`
+ // override, falling back to the book spec's default master.
+ function effectiveMasterIdForPage(entryOrId) {
+ const book = state.book;
+ if (!book) return null;
+ const entry = (typeof entryOrId === 'string')
+ ? book.pages.find(p => p.id === entryOrId)
+ : entryOrId;
+ if (!entry) return book.spec?.defaultMasterId || null;
+ if (entry.masterId === null) return null; // explicit "no master"
+ return entry.masterId || book.spec?.defaultMasterId || null;
+ }
+
// Stable opaque id for masters. Same shape as page entry ids
// so id collisions across pages and masters stay impossible.
function makeBookMasterId() {
@@ -8627,9 +8649,20 @@
thumb.className = 'bookPageTileThumb';
tile.appendChild(thumb);
const pageTarget = `book-page:${p.id}`;
- const pageLayers = (state.layers || [])
- .filter(l => l.target === pageTarget && !l.hidden)
- .map(l => ({ ...l, target: 'canvas' })); // remap for preview helper
+ const masterId = effectiveMasterIdForPage(p);
+ const masterTarget = masterId ? `book-master:${masterId}` : null;
+ // Composite master layers first (so they render below
+ // the page's own layers) then the page's own. Both get
+ // their target remapped to 'canvas' so the preview
+ // helper's flat-canvas filter accepts them.
+ const masterLayers = masterTarget
+ ? (state.layers || []).filter(l => l.target === masterTarget && !l.hidden)
+ : [];
+ const pageLayers = (state.layers || []).filter(l => l.target === pageTarget && !l.hidden);
+ const composed = [
+ ...masterLayers.map((l, i) => ({ ...l, target: 'canvas', z: -1e6 + i })),
+ ...pageLayers.map(l => ({ ...l, target: 'canvas' })),
+ ];
try {
renderCustomArtPreviewDOM(thumb, {
customArt: {
@@ -8637,7 +8670,7 @@
height: trimH * 96,
background: p.background || book.spec?.background || '#ffffff',
},
- layers: pageLayers,
+ layers: composed,
}, { compact: true });
} catch (err) {
console.warn('book page thumb render failed', err);
@@ -8975,13 +9008,18 @@
// is bound to. Targets are id-qualified (`book-page:`
// for pages, `book-master:` for masters) so multiple
// surfaces can coexist in `state.layers` without leaking onto
- // each other.
+ // each other. Page-edit mode also pulls in the page's
+ // effective master so its layers render underneath (the
+ // renderer marks them non-interactive via CSS).
if (mode === 'book') {
if (state.book?.editingMasterId) {
return t === `book-master:${state.book.editingMasterId}`;
}
if (state.book?.editingEntryId) {
- return t === `book-page:${state.book.editingEntryId}`;
+ if (t === `book-page:${state.book.editingEntryId}`) return true;
+ const masterId = effectiveMasterIdForPage(state.book.editingEntryId);
+ if (masterId && t === `book-master:${masterId}`) return true;
+ return false;
}
// Pages overview (no surface open) → no canvas layers render.
return false;
@@ -10270,8 +10308,16 @@
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');
+ // 'editing-book-page' is the shared "canvas is open in book
+ // mode" class — covers both page-edit and master-edit so the
+ // sheet + tool palette unhide. The more specific
+ // 'editing-book-master' class lets CSS target master-edit
+ // alone (e.g. don't fade master layers when *they're* the
+ // editing target).
document.body.classList.toggle('editing-book-page',
state.templateMode === 'book' && (!!state.book?.editingEntryId || !!state.book?.editingMasterId));
+ document.body.classList.toggle('editing-book-master',
+ state.templateMode === 'book' && !!state.book?.editingMasterId);
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);
@@ -18122,7 +18168,18 @@
const designerOverlay = $('designerLayerOverlay');
overlay.innerHTML = '';
if (designerOverlay) designerOverlay.innerHTML = '';
- const ordered = [...state.layers].sort((a, b) => (a.z || 0) - (b.z || 0));
+ // In book page-edit mode, master layers render *under* the
+ // page's own layers regardless of their z values. We offset
+ // their effective sort key by a large negative number so they
+ // always come first in DOM order (which equals lowest visual
+ // stack).
+ const _bookPageEdit = state.templateMode === 'book' && !!state.book?.editingEntryId;
+ const effectiveZ = (layer) => {
+ const z = Number(layer.z) || 0;
+ if (_bookPageEdit && String(layer.target || '').startsWith('book-master:')) return z - 1e6;
+ return z;
+ };
+ const ordered = [...state.layers].sort((a, b) => effectiveZ(a) - effectiveZ(b));
for (const layer of ordered) {
const isDesignerLayer = layer.target === 'designer-disc';
@@ -18139,6 +18196,10 @@
const el = document.createElement('div');
el.className = `freeLayer${layerSelected ? ' selected' : ''}${multiSelected ? ' multiSelected' : ''}`;
el.dataset.layerId = layer.id;
+ // Surface the layer's target on the wrapper so CSS can
+ // style master underlays (.freeLayer[data-target^="book-master:"])
+ // without needing the renderer to know about book mode.
+ if (layer.target) el.dataset.target = layer.target;
if (layer.clip && layer.clip.kind !== 'none' && resolveClipSourceShape(layer, state.layers)) {
el.style.clipPath = `url(#${clipIdForLayer(layer)})`;
}