commit e85bdcd0cc37a32978a1a5da2ee0721b9af3e846
parent 37eb49448871d2fcacbf623e5cfa2103180f83bf
Author: Hunter
Date:   Mon,  2 Feb 2026 18:28:14 -0500

add 3d page flip animation

Diffstat:
Mresources/editor.js | 6++++--
Aresources/page-flip-animation.js | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mresources/preview-manager.js | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mresources/spread-layout.js | 50++++++++++++++++++++++++++++++++++++++++++++++----
Mresources/spread-navigation.js | 30++++++++++++++++++++++++++----
Mresources/viewer-generator.js | 172++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mresources/zine-styles.js | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 576 insertions(+), 29 deletions(-)

diff --git a/resources/editor.js b/resources/editor.js @@ -21,7 +21,8 @@ function updatePreviewFromContent() { // Message handler for iframe communication window.addEventListener('message', function(event) { if (event.data === 'toggleFullscreen') { - toggleFullscreen(updatePreviewWrapper); + toggleFullscreen(); + updatePreviewWrapper(); } else if (event.data === 'prevSpread') { navigateSpread(-1, updatePreviewWrapper); } else if (event.data === 'nextSpread') { @@ -104,7 +105,8 @@ async function initializeEditor() { // Exit fullscreen when viewport is too short (matches CSS @media max-height: 200px) window.addEventListener('resize', function() { if (getIsFullscreen() && window.innerHeight <= 200) { - toggleFullscreen(updatePreviewWrapper); + toggleFullscreen(); + updatePreviewWrapper(); } }); } diff --git a/resources/page-flip-animation.js b/resources/page-flip-animation.js @@ -0,0 +1,168 @@ +// Page flip animation state +let isAnimating = false; +let currentSpreadIndex = 0; + +// Get animation state +export function getIsAnimating() { + return isAnimating; +} + +// Page structure for flip animation +// Each "leaf" has a front and back side +// front-cover/page1 are on opposite sides of the same leaf +// page2/page3 are on opposite sides of the next leaf, etc. +const PAGE_LEAVES = [ + { front: 'front-cover', back: 'page1' }, // Leaf 0 + { front: 'page2', back: 'page3' }, // Leaf 1 + { front: 'page4', back: 'page5' }, // Leaf 2 + { front: 'page6', back: 'back-cover' } // Leaf 3 +]; + +// Initialize the 3D page flip system +export function initPageFlip(container, doc) { + currentSpreadIndex = 0; + + // Create the book structure + const book = doc.createElement('div'); + book.className = 'zine-book'; + + // Create all leaves (page pairs) + PAGE_LEAVES.forEach((leaf, index) => { + const leafEl = doc.createElement('div'); + leafEl.className = 'zine-leaf'; + leafEl.dataset.leafIndex = index; + leafEl.dataset.state = 'closed'; // closed, flipping, open + + // Front side of the leaf + const frontSide = doc.createElement('div'); + frontSide.className = 'zine-leaf-front'; + const frontPage = doc.getElementById(leaf.front); + if (frontPage) { + frontSide.appendChild(frontPage.cloneNode(true)); + } + + // Back side of the leaf + const backSide = doc.createElement('div'); + backSide.className = 'zine-leaf-back'; + const backPage = doc.getElementById(leaf.back); + if (backPage) { + backSide.appendChild(backPage.cloneNode(true)); + } + + leafEl.appendChild(frontSide); + leafEl.appendChild(backSide); + book.appendChild(leafEl); + }); + + container.appendChild(book); + + // Hide original pages in body + const originalPages = doc.querySelectorAll('.page:not(.zine-base-page)'); + originalPages.forEach(page => { + page.style.display = 'none'; + }); + + // Set initial state - all leaves closed, showing front cover + updateLeafStates(0, doc); +} + +// Update which leaves are open/closed based on current spread +function updateLeafStates(spreadIndex, doc) { + const leaves = doc.querySelectorAll('.zine-leaf'); + + leaves.forEach((leaf, index) => { + // Spread 0 = front cover (no leaves flipped) + // Spread 1 = pages 1-2 (leaf 0 flipped) + // Spread 2 = pages 3-4 (leaves 0-1 flipped) + // Spread 3 = pages 5-6 (leaves 0-2 flipped) + // Spread 4 = back cover (all leaves flipped) + + if (index < spreadIndex) { + // This leaf should be flipped to the left (showing back) + leaf.dataset.state = 'open'; + leaf.style.transform = 'rotateY(-180deg)'; + // Open leaves on left have lower z-index + leaf.style.zIndex = String(index + 1); + } else { + // This leaf should be closed (on the right, showing front) + leaf.dataset.state = 'closed'; + leaf.style.transform = 'rotateY(0deg)'; + // Closed leaves stack with first on top + leaf.style.zIndex = String(20 - index); + } + }); + +} + +// Animate page flip +export function animatePageFlip(fromSpread, toSpread, container, doc, onComplete) { + if (isAnimating) return; + + isAnimating = true; + currentSpreadIndex = toSpread; + + const direction = toSpread > fromSpread ? 'forward' : 'backward'; + const leaves = doc.querySelectorAll('.zine-leaf'); + + if (direction === 'forward') { + // Flipping forward (right to left) + const leafToFlip = leaves[fromSpread]; + if (leafToFlip) { + leafToFlip.dataset.state = 'flipping'; + leafToFlip.style.zIndex = '100'; // Put it on top during flip + + // Enable transition + leafToFlip.style.transition = 'transform 0.6s cubic-bezier(0.645, 0.045, 0.355, 1.000)'; + + // Trigger the flip + requestAnimationFrame(() => { + leafToFlip.style.transform = 'rotateY(-180deg)'; + }); + + // Clean up after animation + const cleanup = () => { + leafToFlip.dataset.state = 'open'; + leafToFlip.style.transition = ''; + // Set z-index for open state (lower than closed leaves) + leafToFlip.style.zIndex = String(fromSpread + 1); + isAnimating = false; + if (onComplete) onComplete(); + leafToFlip.removeEventListener('transitionend', cleanup); + }; + leafToFlip.addEventListener('transitionend', cleanup); + } + } else { + // Flipping backward (left to right) + const leafToFlip = leaves[toSpread]; + if (leafToFlip) { + leafToFlip.dataset.state = 'flipping'; + leafToFlip.style.zIndex = '100'; // Put it on top during flip + + // Enable transition + leafToFlip.style.transition = 'transform 0.6s cubic-bezier(0.645, 0.045, 0.355, 1.000)'; + + // Trigger the flip + requestAnimationFrame(() => { + leafToFlip.style.transform = 'rotateY(0deg)'; + }); + + // Clean up after animation + const cleanup = () => { + leafToFlip.dataset.state = 'closed'; + leafToFlip.style.transition = ''; + // Set z-index for closed state (higher than open leaves) + leafToFlip.style.zIndex = String(20 - toSpread); + isAnimating = false; + if (onComplete) onComplete(); + leafToFlip.removeEventListener('transitionend', cleanup); + }; + leafToFlip.addEventListener('transitionend', cleanup); + } + } +} + +// Set current spread without animation (for initial load) +export function setSpreadImmediate(spreadIndex, doc) { + currentSpreadIndex = spreadIndex; + updateLeafStates(spreadIndex, doc); +} diff --git a/resources/preview-manager.js b/resources/preview-manager.js @@ -1,10 +1,63 @@ import { insertAfterHead } from './html-utils.js'; import { extractTitleAndFavicon, updateMainPageTitleAndFavicon } from './html-utils.js'; -import { generateSpreadCSS } from './zine-styles.js'; +import { generateSpreadCSS, ZINE_PAGE_CSS, ZINE_FLIP_CSS, ZINE_PRINT_CSS } from './zine-styles.js'; import { SPREADS } from './constants.js'; import { getCurrentSpread } from './spread-navigation.js'; import { isMobileDevice } from './mobile-keyboard.js'; -import { setupSpreadLayout, scaleSpreadToFit } from './spread-layout.js'; +import { setupSpreadLayout, scaleSpreadToFit, navigateToSpread } from './spread-layout.js'; +import { setSpreadImmediate } from './page-flip-animation.js'; + +// Track if flip mode is enabled +let flipModeEnabled = true; // Default to enabled +let previousSpread = 0; + +export function setFlipMode(enabled) { + flipModeEnabled = enabled; +} + +export function isFlipModeEnabled() { + return flipModeEnabled; +} + +// Generate CSS for flip mode +function generateFlipModeCSS() { + return ` + * { margin: 0; padding: 0; box-sizing: border-box; } + + @media screen { + html, body { + height: 100% !important; + overflow: hidden !important; + } + + body { + display: flex !important; + justify-content: center !important; + align-items: center !important; + } + + body > *:not(.zine-spread-container):not(.iframe-fullscreen-toggle) { + display: none !important; + } + + ${ZINE_PAGE_CSS} + ${ZINE_FLIP_CSS} + + .zine-spread-container { + display: flex !important; + position: relative; + } + + .page { + display: block !important; + } + } + + @media print { + ${ZINE_PRINT_CSS} + } + `; +} export function updatePreview(editorView, isEditorFocused) { const preview = document.getElementById('preview'); @@ -29,7 +82,7 @@ export function updatePreview(editorView, isEditorFocused) { // Inject spread CSS right after <head> so user styles come last and take precedence const currentSpread = getCurrentSpread(); - const spreadCSS = generateSpreadCSS(currentSpread); + const spreadCSS = flipModeEnabled ? generateFlipModeCSS() : generateSpreadCSS(currentSpread); const processedCode = insertAfterHead(code, `<style id="zine-editor-spread-css">${spreadCSS}</style>`); preview.srcdoc = processedCode || '<!DOCTYPE html><html><head></head><body></body></html>'; @@ -65,13 +118,32 @@ export function updatePreview(editorView, isEditorFocused) { const container = doc.createElement('div'); container.className = 'zine-spread-container'; - setupSpreadLayout(container, spread, doc); - doc.body.appendChild(container); + if (flipModeEnabled) { + // Setup flip animation mode + setupSpreadLayout(container, spread, doc, true); + doc.body.appendChild(container); + + // Set initial spread state without animation + setTimeout(() => { + setSpreadImmediate(currentSpread, doc); + const scaleToFit = () => scaleSpreadToFit(container, doc); + scaleToFit(); + doc.defaultView.addEventListener('resize', scaleToFit); + }, 0); + } else { + // Original non-animated mode + setupSpreadLayout(container, spread, doc, false); + doc.body.appendChild(container); + + // Scale container to fit viewport + const scaleToFit = () => scaleSpreadToFit(container, doc); + scaleToFit(); + doc.defaultView.addEventListener('resize', scaleToFit); + } - // Scale container to fit viewport - const scaleToFit = () => scaleSpreadToFit(container, doc); - scaleToFit(); - doc.defaultView.addEventListener('resize', scaleToFit); + // Store container and flip mode state for navigation updates + doc._zineContainer = container; + doc._flipModeEnabled = flipModeEnabled; } // Add keyboard listener for navigation and shortcuts diff --git a/resources/spread-layout.js b/resources/spread-layout.js @@ -1,8 +1,31 @@ import { SPREADS } from './constants.js'; import { createEmptyPlaceholder } from './html-utils.js'; +import { initPageFlip, animatePageFlip, setSpreadImmediate, getIsAnimating } from './page-flip-animation.js'; + +// Track whether flip mode is enabled +let flipModeEnabled = false; +let currentFlipSpread = 0; + +// Enable/disable flip animation mode +export function setFlipMode(enabled) { + flipModeEnabled = enabled; +} + +export function isFlipModeEnabled() { + return flipModeEnabled; +} // Common logic for displaying a spread (used by both editor preview and standalone viewer) -export function setupSpreadLayout(container, spread, doc) { +export function setupSpreadLayout(container, spread, doc, useFlipAnimation = false) { + if (useFlipAnimation) { + // Initialize flip animation on first call + if (!container.querySelector('.zine-book')) { + initPageFlip(container, doc); + } + return; + } + + // Original non-animated behavior // Add empty placeholder before right page if no left page if (!spread.left && spread.right) { container.appendChild(createEmptyPlaceholder(doc)); @@ -21,15 +44,34 @@ export function setupSpreadLayout(container, spread, doc) { } } +// Navigate to a spread with optional flip animation +export function navigateToSpread(container, spreadIndex, doc, useFlipAnimation = false, onComplete) { + if (useFlipAnimation && container.querySelector('.zine-book')) { + if (!getIsAnimating()) { + animatePageFlip(currentFlipSpread, spreadIndex, container, doc, onComplete); + currentFlipSpread = spreadIndex; + } + } else { + // Original non-animated behavior + const spread = SPREADS[spreadIndex]; + container.innerHTML = ''; + setupSpreadLayout(container, spread, doc, false); + if (onComplete) onComplete(); + } +} + // Scale spread container to fit viewport export function scaleSpreadToFit(container, doc, bottomPadding = 0) { + // Find the book container if in flip mode + const bookContainer = container.querySelector('.zine-book') || container; + const vw = doc.documentElement.clientWidth; const vh = doc.documentElement.clientHeight - bottomPadding; - const spreadWidthPx = container.offsetWidth; - const spreadHeightPx = container.offsetHeight; + const spreadWidthPx = bookContainer.offsetWidth; + const spreadHeightPx = bookContainer.offsetHeight; if (spreadWidthPx === 0 || spreadHeightPx === 0) return; const scaleX = (vw - 40) / spreadWidthPx; const scaleY = (vh - 40) / spreadHeightPx; const scale = Math.min(scaleX, scaleY); - container.style.transform = `scale(${scale})`; + bookContainer.style.transform = `scale(${scale})`; } diff --git a/resources/spread-navigation.js b/resources/spread-navigation.js @@ -41,14 +41,36 @@ export function navigateSpread(delta, updatePreviewCallback) { if (newSpread >= 0 && newSpread < SPREADS.length) { currentSpread = newSpread; updateSpreadIndicator(); - updatePreviewCallback(); - // Refocus preview so keyboard navigation continues to work - document.getElementById('preview').focus(); + + // Try to animate if flip mode is enabled + const preview = document.getElementById('preview'); + try { + const doc = preview.contentDocument; + const container = doc?._zineContainer; + const flipModeEnabled = doc?._flipModeEnabled; + + if (container && flipModeEnabled) { + // Dynamic import to avoid circular dependency + import('./spread-layout.js').then(({ navigateToSpread }) => { + navigateToSpread(container, newSpread, doc, true, () => { + // Refocus preview so keyboard navigation continues to work + preview.focus(); + }); + }); + } else { + updatePreviewCallback(); + preview.focus(); + } + } catch (e) { + // Fallback to regular update + updatePreviewCallback(); + preview.focus(); + } } } // Fullscreen toggle -export function toggleFullscreen(updatePreviewCallback) { +export function toggleFullscreen() { isFullscreen = !isFullscreen; const editorPane = document.querySelector('.editor-pane'); const previewPane = document.querySelector('.preview-pane'); diff --git a/resources/viewer-generator.js b/resources/viewer-generator.js @@ -1,5 +1,5 @@ import { PAGE_IDS, SPREADS, NAV_HEIGHT, NAV_BUTTON_CSS, NAV_BUTTON_STYLES } from './constants.js'; -import { ZINE_PAGE_CSS, ZINE_PRINT_CSS } from './zine-styles.js'; +import { ZINE_PAGE_CSS, ZINE_PRINT_CSS, ZINE_FLIP_CSS } from './zine-styles.js'; // Generate standalone viewer CSS and JS (matches preview pane rendering) export function generateViewerCode() { @@ -22,6 +22,19 @@ export function generateViewerCode() { display: none !important; } ${ZINE_PAGE_CSS} + ${ZINE_FLIP_CSS} + + /* Dynamic z-index based on leaf state */ + .zine-leaf[data-state="closed"][data-leaf-index="0"] { z-index: 14 !important; } + .zine-leaf[data-state="closed"][data-leaf-index="1"] { z-index: 13 !important; } + .zine-leaf[data-state="closed"][data-leaf-index="2"] { z-index: 12 !important; } + .zine-leaf[data-state="closed"][data-leaf-index="3"] { z-index: 11 !important; } + .zine-leaf[data-state="open"][data-leaf-index="0"] { z-index: 4 !important; } + .zine-leaf[data-state="open"][data-leaf-index="1"] { z-index: 3 !important; } + .zine-leaf[data-state="open"][data-leaf-index="2"] { z-index: 2 !important; } + .zine-leaf[data-state="open"][data-leaf-index="3"] { z-index: 1 !important; } + .zine-leaf[data-state="flipping"] { z-index: 20 !important; } + .zine-spread-container { display: flex !important; position: relative; @@ -65,25 +78,141 @@ export function generateViewerCode() { const NAV_HEIGHT = ${NAV_HEIGHT}; let currentSpread = 0; let container; + let isAnimating = false; + const USE_FLIP_ANIMATION = true; + + // Page leaf structure (front and back of each physical page) + const PAGE_LEAVES = [ + { front: 'front-cover', back: 'page1' }, + { front: 'page2', back: 'page3' }, + { front: 'page4', back: 'page5' }, + { front: 'page6', back: 'back-cover' } + ]; + + function easeInOutCubic(t) { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; + } function scaleToFit() { if (!container) return; + const bookContainer = container.querySelector('.zine-book') || container; const vw = document.documentElement.clientWidth; const vh = document.documentElement.clientHeight - NAV_HEIGHT; - const spreadWidthPx = container.offsetWidth; - const spreadHeightPx = container.offsetHeight; + const spreadWidthPx = bookContainer.offsetWidth; + const spreadHeightPx = bookContainer.offsetHeight; if (spreadWidthPx === 0 || spreadHeightPx === 0) return; const scaleX = (vw - 40) / spreadWidthPx; const scaleY = (vh - 40) / spreadHeightPx; const scale = Math.min(scaleX, scaleY); - container.style.transform = 'scale(' + scale + ')'; + bookContainer.style.transform = 'scale(' + scale + ')'; + } + + function initFlipMode() { + const book = document.createElement('div'); + book.className = 'zine-book'; + + // Create all leaves + PAGE_LEAVES.forEach((leaf, index) => { + const leafEl = document.createElement('div'); + leafEl.className = 'zine-leaf'; + leafEl.dataset.leafIndex = index; + leafEl.dataset.state = 'closed'; + leafEl.style.zIndex = String(20 - index); // Initial z-index for closed state + + const frontSide = document.createElement('div'); + frontSide.className = 'zine-leaf-front'; + const frontPage = document.getElementById(leaf.front); + if (frontPage) frontSide.appendChild(frontPage.cloneNode(true)); + + const backSide = document.createElement('div'); + backSide.className = 'zine-leaf-back'; + const backPage = document.getElementById(leaf.back); + if (backPage) backSide.appendChild(backPage.cloneNode(true)); + + leafEl.appendChild(frontSide); + leafEl.appendChild(backSide); + book.appendChild(leafEl); + }); + + container.appendChild(book); } - function showSpread(index) { + function animateFlip(fromSpread, toSpread, onComplete) { + if (isAnimating) return; + isAnimating = true; + + const direction = toSpread > fromSpread ? 'forward' : 'backward'; + const leaves = document.querySelectorAll('.zine-leaf'); + + if (direction === 'forward') { + const leafToFlip = leaves[fromSpread]; + if (leafToFlip) { + leafToFlip.dataset.state = 'flipping'; + leafToFlip.style.zIndex = '100'; + const startTime = performance.now(); + const duration = 600; + + function animate(currentTime) { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + const eased = easeInOutCubic(progress); + const rotation = eased * -180; + leafToFlip.style.transform = 'rotateY(' + rotation + 'deg)'; + + if (progress < 1) { + requestAnimationFrame(animate); + } else { + leafToFlip.dataset.state = 'open'; + leafToFlip.style.zIndex = String(fromSpread + 1); + isAnimating = false; + if (onComplete) onComplete(); + } + } + requestAnimationFrame(animate); + } + } else { + const leafToFlip = leaves[toSpread]; + if (leafToFlip) { + leafToFlip.dataset.state = 'flipping'; + leafToFlip.style.zIndex = '100'; + const startTime = performance.now(); + const duration = 600; + + function animate(currentTime) { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + const eased = easeInOutCubic(progress); + const rotation = -180 + (eased * 180); + leafToFlip.style.transform = 'rotateY(' + rotation + 'deg)'; + + if (progress < 1) { + requestAnimationFrame(animate); + } else { + leafToFlip.dataset.state = 'closed'; + leafToFlip.style.zIndex = String(20 - toSpread); + isAnimating = false; + if (onComplete) onComplete(); + } + } + requestAnimationFrame(animate); + } + } + } + + function showSpread(index, animated) { const spread = SPREADS[index]; - const visible = [spread.left, spread.right].filter(Boolean); - // Move existing pages back to body before clearing container + if (USE_FLIP_ANIMATION && animated && container.querySelector('.zine-book')) { + animateFlip(currentSpread, index, () => { + document.getElementById('zine-indicator').textContent = spread.label; + document.getElementById('zine-prev').disabled = index === 0; + document.getElementById('zine-next').disabled = index === SPREADS.length - 1; + }); + return; + } + + // Non-animated or initial setup + const visible = [spread.left, spread.right].filter(Boolean); PAGE_IDS.forEach(id => { const page = document.getElementById(id); if (page) { @@ -123,8 +252,9 @@ export function generateViewerCode() { function navigate(delta) { const newIndex = currentSpread + delta; if (newIndex >= 0 && newIndex < SPREADS.length) { + const oldSpread = currentSpread; currentSpread = newIndex; - showSpread(currentSpread); + showSpread(currentSpread, true); } } @@ -179,7 +309,20 @@ export function generateViewerCode() { document.getElementById('zine-prev').addEventListener('click', () => navigate(-1)); document.getElementById('zine-next').addEventListener('click', () => navigate(1)); - showSpread(currentSpread); + if (USE_FLIP_ANIMATION) { + initFlipMode(); + const leaves = document.querySelectorAll('.zine-leaf'); + leaves.forEach((leaf, index) => { + if (index < currentSpread) { + leaf.dataset.state = 'open'; + leaf.style.transform = 'rotateY(-180deg)'; + leaf.style.zIndex = String(index + 1); + } + }); + } else { + showSpread(currentSpread, false); + } + scaleToFit(); const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); @@ -193,7 +336,16 @@ export function generateViewerCode() { window.addEventListener('resize', scaleToFit); - showSpread(0); + // Initialize flip mode + if (USE_FLIP_ANIMATION) { + initFlipMode(); + document.getElementById('zine-indicator').textContent = SPREADS[0].label; + document.getElementById('zine-prev').disabled = true; + document.getElementById('zine-next').disabled = false; + scaleToFit(); + } else { + showSpread(0, false); + } }); `; diff --git a/resources/zine-styles.js b/resources/zine-styles.js @@ -41,6 +41,95 @@ export const ZINE_PRINT_CSS = ` a { color: black; } `; +// CSS for 3D page flip animation +export const ZINE_FLIP_CSS = ` + .zine-book { + position: relative; + width: ${PAGE_WIDTH_IN * 2}in; + height: ${PAGE_HEIGHT_IN}in; + perspective: 2000px; + transform-style: preserve-3d; + transform-origin: center center; + } + + .zine-leaf { + position: absolute; + width: ${PAGE_WIDTH_IN}in; + height: ${PAGE_HEIGHT_IN}in; + left: ${PAGE_WIDTH_IN}in; + top: 0; + transform-origin: left center; + transform-style: preserve-3d; + transition: none; + } + + /* When flipping, use high z-index so it's on top during animation */ + .zine-leaf[data-state="flipping"] { + z-index: 20 !important; + transition: none; + } + + .zine-leaf-front, + .zine-leaf-back { + position: absolute; + width: 100%; + height: 100%; + backface-visibility: hidden; + -webkit-backface-visibility: hidden; + } + + .zine-leaf-front { + transform: rotateY(0deg); + z-index: 2; + } + + .zine-leaf-back { + transform: rotateY(180deg); + z-index: 1; + } + + .zine-leaf-front .page, + .zine-leaf-back .page { + width: 100%; + height: 100%; + display: block !important; + } + + /* Base pages - shown beneath the flipping leaves */ + .zine-base-pages { + position: absolute; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + z-index: 0; + } + + .zine-base-page { + position: absolute; + width: ${PAGE_WIDTH_IN}in; + height: ${PAGE_HEIGHT_IN}in; + display: none !important; + z-index: 0; + } + + /* Position base pages */ + .zine-base-page[data-page-id="front-cover"], + .zine-base-page[data-page-id="page2"], + .zine-base-page[data-page-id="page4"], + .zine-base-page[data-page-id="page6"] { + right: 0; + } + + .zine-base-page[data-page-id="page1"], + .zine-base-page[data-page-id="page3"], + .zine-base-page[data-page-id="page5"], + .zine-base-page[data-page-id="back-cover"] { + left: 0; + } +`; + // Shared CSS for zine pages (used by both editor preview and exported viewer) export const ZINE_PAGE_CSS = ` ${ALL_PAGES_SELECTOR} {