mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
book(A5): Cover view with spec-derived spread preview
Cover view shows a live, proportional preview of the front / spine / back spread alongside a readout of every spec value that produced it. Panel widths track the book.spec values exactly: - Spine width derives via the existing bookSpineWidthInches() helper — page count × paper thickness when spineFromPages is on, otherwise the manual override. The summary readout marks the derived value in accent color with a "derived: N pages × T in" caption. - The spread reflows automatically: change paper thickness in Document settings or add / remove pages, the spine column resizes, the readout updates. Same when toggling coverHasSpine or coverHasBack — the spread becomes back+spine+front / spine+front / back+front / front only. - Bleed adds to each outer edge of the back and front panels; the inner edges between panels share so no double-bleed. - A scaled px-per-inch factor fits the spread into ~720 px wide; scaling caps at 60 px/in so a tiny chapbook isn't huge. - Cover view's renderBookCoverPreview() is guarded inside render() — it only repaints when the Cover sub-view is visible, so flipping between Pages and Cover stays cheap. Phase A5 ships preview-only. Phase B mounts the existing cover designer host inside #bookCoverView so users can actually paint the panels; Phase D1 publishes the cover as its own casewrap-page event with role:'cover-spread'. A "Replace cover from Nostr…" button is stubbed disabled to anchor the future picker in the UI.
This commit is contained in:
1 parent
609ffbb6af
commit
382165d21b
1 file changed
+162
-1
+162
-1
@@ -1546,6 +1546,73 @@
|
||||
font-size: .8rem; color: var(--muted); text-align: center;
|
||||
padding: 0 8px;
|
||||
}
|
||||
/* Cover view: a live-derived spread preview + readout. */
|
||||
.bookCoverSummary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px 18px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0;
|
||||
background: var(--bg);
|
||||
font-size: .82rem;
|
||||
}
|
||||
.bookCoverSummary dt {
|
||||
font-size: .72rem; color: var(--muted);
|
||||
text-transform: uppercase; letter-spacing: .04em;
|
||||
margin: 0 0 2px;
|
||||
}
|
||||
.bookCoverSummary dd {
|
||||
font-weight: 600; margin: 0;
|
||||
}
|
||||
.bookCoverSummary dd.derived { color: var(--accent); }
|
||||
.bookCoverSummary dd .smallNote {
|
||||
font-weight: 400; color: var(--muted); font-size: .72rem;
|
||||
display: block; margin-top: 2px;
|
||||
}
|
||||
.bookCoverPreviewWrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
padding: 20px 0;
|
||||
display: flex; justify-content: center;
|
||||
}
|
||||
.bookCoverPreview {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.08);
|
||||
}
|
||||
.bookCoverPanel {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--line);
|
||||
background: #ffffff;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.bookCoverPanel + .bookCoverPanel { border-left: none; }
|
||||
.bookCoverPanel.back { background: rgba(125,211,252,.10); }
|
||||
.bookCoverPanel.spine { background: rgba(192,132,252,.12); }
|
||||
.bookCoverPanel.front { background: rgba(167,243,208,.12); }
|
||||
.bookCoverPanelLabel {
|
||||
font-size: .72rem; color: var(--muted);
|
||||
text-transform: uppercase; letter-spacing: .08em;
|
||||
writing-mode: horizontal-tb;
|
||||
text-align: center; padding: 6px;
|
||||
}
|
||||
.bookCoverPanel.spine .bookCoverPanelLabel {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.bookCoverPanelDim {
|
||||
position: absolute; bottom: 4px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: .68rem; color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bookCoverPanel.spine .bookCoverPanelDim {
|
||||
bottom: auto; top: 50%; left: auto; right: 4px;
|
||||
transform: translateY(-50%) rotate(-90deg);
|
||||
transform-origin: right center;
|
||||
}
|
||||
|
||||
.templateLayerPanels {
|
||||
display: grid;
|
||||
@@ -4108,7 +4175,14 @@
|
||||
<p class="hint" id="bookPagesHint" style="margin:8px 0 0">Phase A4 skeleton: tiles are placeholders. Click a tile in Phase B to edit the page. Reflow / NIP-23 chapters arrive in Phase C.5; per-page Nostr publish in Phase D1.</p>
|
||||
</div>
|
||||
<div id="bookCoverView" style="display:none">
|
||||
<p class="hint">Cover view ships in Phase A5. The cover designer (front + spine + back) will mount here, with the spine column derived from page count × paper thickness.</p>
|
||||
<div class="bookCoverSummary" id="bookCoverSummary"></div>
|
||||
<div class="bookCoverPreviewWrap">
|
||||
<div class="bookCoverPreview" id="bookCoverPreview" aria-label="Cover spread preview"></div>
|
||||
</div>
|
||||
<div class="buttonRow" style="margin-top:8px">
|
||||
<button type="button" id="bookCoverReplaceBtn" disabled title="Phase D1 — pick from your published cover-spread events">Replace cover from Nostr…</button>
|
||||
</div>
|
||||
<p class="hint" style="margin-top:8px">The cover spread reflows automatically when page count or paper thickness changes (spine width re-derives) and when trim or bleed changes in Document settings. Editing the cover layers mounts the cover designer in Phase B; publishing the cover as its own <code>casewrap-page</code> event lands in Phase D1.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8274,6 +8348,87 @@
|
||||
if (cover) cover.style.display = showCover ? '' : 'none';
|
||||
if (tPages) tPages.classList.toggle('active', !showCover);
|
||||
if (tCover) tCover.classList.toggle('active', showCover);
|
||||
if (showCover) renderBookCoverPreview();
|
||||
}
|
||||
|
||||
// Cover spread preview. Lays out back / spine / front panels with
|
||||
// pixel widths proportional to their spec dimensions. The spine
|
||||
// width derives from page count × paper thickness when
|
||||
// spineFromPages is on (the existing `bookSpineWidthInches()`
|
||||
// helper handles either mode). A readout above the spread shows
|
||||
// every spec value so the user sees the math behind the geometry.
|
||||
function renderBookCoverPreview() {
|
||||
const wrap = document.getElementById('bookCoverPreview');
|
||||
const summary = document.getElementById('bookCoverSummary');
|
||||
const book = state.book;
|
||||
if (!wrap || !summary || !book) return;
|
||||
const s = book.spec;
|
||||
const spineW = bookSpineWidthInches();
|
||||
const pageCount = bookPageCount();
|
||||
const trimW = Number(s.trimW) || 0;
|
||||
const trimH = Number(s.trimH) || 0;
|
||||
const bleed = Number(s.bleed) || 0;
|
||||
const hasSpine = !!s.coverHasSpine;
|
||||
const hasBack = !!s.coverHasBack;
|
||||
|
||||
// Total spread width = (back?) + (spine?) + front, each
|
||||
// including its own outer bleed; the inner edges between
|
||||
// panels touch (no double-bleed).
|
||||
const totalW = (hasBack ? trimW + bleed : 0)
|
||||
+ (hasSpine ? spineW : 0)
|
||||
+ (trimW + bleed)
|
||||
+ (hasBack ? 0 : bleed);
|
||||
const totalH = trimH + bleed * 2;
|
||||
|
||||
// Summary block.
|
||||
const fmt = (n) => Number(n).toFixed(n % 1 === 0 ? 0 : 3);
|
||||
summary.innerHTML = [
|
||||
['Book size', `${s.bookSize || 'Custom'}`],
|
||||
['Trim', `${fmt(trimW)} × ${fmt(trimH)} in`],
|
||||
['Bleed', `${fmt(bleed)} in / edge`],
|
||||
['Spine', `${fmt(spineW)} in`,
|
||||
s.spineFromPages
|
||||
? `derived: ${pageCount} pages × ${fmt(s.paperThickness)} in`
|
||||
: 'manual'
|
||||
],
|
||||
['Spread', `${fmt(totalW)} × ${fmt(totalH)} in`,
|
||||
hasSpine ? (hasBack ? 'back + spine + front' : 'spine + front') : (hasBack ? 'back + front' : 'front only')
|
||||
],
|
||||
['Pages', `${pageCount}`],
|
||||
].map(([dt, dd, note]) => `
|
||||
<div>
|
||||
<dt>${dt}</dt>
|
||||
<dd${note && dt === 'Spine' && s.spineFromPages ? ' class="derived"' : ''}>${dd}${note ? `<span class="smallNote">${note}</span>` : ''}</dd>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Spread preview. Scale to fit in ~720 px wide; min 200 px.
|
||||
const previewMaxPx = 720;
|
||||
const pxPerIn = totalW > 0 ? Math.min(previewMaxPx / totalW, 60) : 60;
|
||||
const heightPx = Math.max(40, totalH * pxPerIn);
|
||||
wrap.innerHTML = '';
|
||||
|
||||
const addPanel = (cls, label, widthIn, dim) => {
|
||||
const panel = document.createElement('div');
|
||||
panel.className = `bookCoverPanel ${cls}`;
|
||||
panel.style.width = `${widthIn * pxPerIn}px`;
|
||||
panel.style.height = `${heightPx}px`;
|
||||
const lab = document.createElement('div');
|
||||
lab.className = 'bookCoverPanelLabel';
|
||||
lab.textContent = label;
|
||||
panel.appendChild(lab);
|
||||
if (dim) {
|
||||
const dimEl = document.createElement('div');
|
||||
dimEl.className = 'bookCoverPanelDim';
|
||||
dimEl.textContent = dim;
|
||||
panel.appendChild(dimEl);
|
||||
}
|
||||
wrap.appendChild(panel);
|
||||
};
|
||||
|
||||
if (hasBack) addPanel('back', 'Back', trimW + bleed, `${fmt(trimW)} in`);
|
||||
if (hasSpine && spineW > 0) addPanel('spine', 'Spine', spineW, `${fmt(spineW)} in`);
|
||||
addPanel('front', 'Front', trimW + bleed, `${fmt(trimW)} in`);
|
||||
}
|
||||
|
||||
function syncBookSidebarSummary() {
|
||||
@@ -12280,6 +12435,12 @@
|
||||
updateBodyClasses();
|
||||
renderDeckSorter();
|
||||
renderBookPagesGrid();
|
||||
// Only repaint the cover preview when its view is the active
|
||||
// sub-view — saves work on every render() in book mode.
|
||||
if (document.getElementById('bookCoverView')?.style.display !== 'none'
|
||||
&& state.templateMode === 'book') {
|
||||
renderBookCoverPreview();
|
||||
}
|
||||
renderCreditsPanel();
|
||||
if (state.templateMode === 'pixelart') ensureStandalonePixelEditor();
|
||||
// After updateBodyClasses so the designer view is displayed/laid out —
|
||||
|
||||
Reference in new issue
Block a user