From f28f0d92e8e959e007bd3d9b8dd415900ddd0198 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:23:13 -0400 Subject: [PATCH] book(C3): master-layer underlay on page editor + composited thumbnails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pages inheriting a master now render its layers *under* their own, in both the per-page editor and the Pages-overview tile thumbnails. - New `effectiveMasterIdForPage(entryOrId)` resolves the page's master: its own `masterId` override → book spec's `defaultMasterId` → null. Setting `entry.masterId = null` explicitly means "no master on this page" (overrides default). - `isLayerForCurrentMode` in page-edit mode now also accepts the page's effective-master target so those layers join the render. - `renderLayers` uses an effective-z sort key that subtracts 1e6 from master-layer z values while in page-edit mode — guarantees master layers DOM-precede page layers regardless of authored z, so masters always sit beneath the page artwork. - New body class `editing-book-master` (in addition to the shared `editing-book-page`) lets CSS tell page-edit from master-edit. CSS rule fades + click-throughs master layers only in page-edit; master-edit mode keeps them fully interactive (you're editing the master itself). - Free-layer wrappers now carry `data-target` so the CSS selector `[data-target^="book-master:"]` works without needing the renderer to know about book mode. - Pages-overview tile thumbnails composite the master layers beneath the page's own (z forced to -1e6 + i for stable order) so each tile shows the same composition the editor renders. Phase C3 ships render-side composition; the per-page "Apply master…" picker (entry.masterId override UI) is queued for the next slice. --- src/index.html | 75 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 7 deletions(-) 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)})`; }