fix(rpubs): fix flipbook white screen and align flow with /press landing
- Flipbook: wrap async initFlipbook in try-catch with scroll fallback, add 500ms verification timeout for silent StPageFlip failures, quote data URLs in background-image, expand shadow DOM CSS coverage - Flow: rename steps Create/Preview/Publish → Write/Press/Print to match rpubs.online/press landing page description - Routes: add /press editor route, bump script cache to v=3 - Publish panel: fix getEditorContent() reading from cached content instead of missing textarea (not rendered during print step) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1cc083a655
commit
636360ce5f
|
|
@ -83,6 +83,11 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
private _pdfInfo: string | null = null;
|
||||
private _pdfPageCount = 0;
|
||||
|
||||
// ── Cached content (for publish panel access when textarea isn't in DOM) ──
|
||||
private _cachedContent = "";
|
||||
private _cachedTitle = "";
|
||||
private _cachedAuthor = "";
|
||||
|
||||
// ── Wizard view state ──
|
||||
private _view: "write" | "preview" | "publish" = "write";
|
||||
|
||||
|
|
@ -104,7 +109,7 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
private static readonly TOUR_STEPS = [
|
||||
{ target: '.content-area', title: "Editor", message: "Write or paste markdown content here. Drag-and-drop text files also works.", advanceOnClick: false },
|
||||
{ target: '.format-dropdown-btn', title: "Format", message: "Choose a pocket-book format — digest, half-letter, A6, and more.", advanceOnClick: false },
|
||||
{ target: '.btn-generate', title: "Generate PDF", message: "Generate a print-ready PDF and advance to the preview step.", advanceOnClick: false },
|
||||
{ target: '.btn-generate', title: "Press It", message: "Generate a print-ready PDF and advance to the press step.", advanceOnClick: false },
|
||||
{ target: '.drafts-dropdown-btn', title: "Drafts", message: "Save multiple drafts with real-time collaborative sync.", advanceOnClick: false },
|
||||
{ target: '.btn-zine-gen', title: "Zine Generator", message: "Generate an AI-illustrated 8-page zine — pick a topic, style, and tone, then edit any section before printing.", advanceOnClick: false },
|
||||
];
|
||||
|
|
@ -129,6 +134,9 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
}
|
||||
};
|
||||
|
||||
/** Expose cached content for the publish panel (which can't access the textarea when it's not rendered) */
|
||||
get cachedContent() { return { content: this._cachedContent, title: this._cachedTitle, author: this._cachedAuthor }; }
|
||||
|
||||
set formats(val: BookFormat[]) {
|
||||
this._formats = val;
|
||||
if (this.shadowRoot) this.render();
|
||||
|
|
@ -468,17 +476,17 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
<div class="steps">
|
||||
<button class="step ${this._view === 'write' ? 'active' : ''} ${this._pdfUrl && this._view !== 'write' ? 'done' : ''}" data-step="write">
|
||||
<span class="step-circle">${this._pdfUrl && this._view !== 'write' ? SVG_CHECK : '1'}</span>
|
||||
<span class="step-label">Create</span>
|
||||
<span class="step-label">Write</span>
|
||||
</button>
|
||||
<div class="step-line ${this._view !== 'write' ? 'filled' : ''}"></div>
|
||||
<button class="step ${this._view === 'preview' ? 'active' : ''} ${this._view === 'publish' ? 'done' : ''}" data-step="preview" ${!this._pdfUrl ? 'disabled' : ''}>
|
||||
<span class="step-circle">${this._view === 'publish' ? SVG_CHECK : '2'}</span>
|
||||
<span class="step-label">Preview</span>
|
||||
<span class="step-label">Press</span>
|
||||
</button>
|
||||
<div class="step-line ${this._view === 'publish' ? 'filled' : ''}"></div>
|
||||
<button class="step ${this._view === 'publish' ? 'active' : ''}" data-step="publish" ${!this._pdfUrl ? 'disabled' : ''}>
|
||||
<span class="step-circle">3</span>
|
||||
<span class="step-label">Publish</span>
|
||||
<span class="step-label">Print</span>
|
||||
</button>
|
||||
</div>
|
||||
${this._pdfInfo ? `<span class="step-info">${this._pdfInfo}</span>` : ''}
|
||||
|
|
@ -515,8 +523,8 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
${this._error ? `<div class="inline-error">${this.escapeHtml(this._error)}</div>` : ''}
|
||||
<button class="btn-generate" ${this._loading ? "disabled" : ""}>
|
||||
${this._loading
|
||||
? `${SVG_SPINNER} Generating...`
|
||||
: `Generate Preview ${SVG_ARROW_RIGHT}`}
|
||||
? `${SVG_SPINNER} Pressing...`
|
||||
: `Press It ${SVG_ARROW_RIGHT}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -539,7 +547,7 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
</div>
|
||||
<div class="preview-bottom-bar">
|
||||
<span class="preview-info">${this._pdfInfo || ''}</span>
|
||||
<button class="btn-publish-next">Publish ${SVG_ARROW_RIGHT}</button>
|
||||
<button class="btn-publish-next">Print Locally ${SVG_ARROW_RIGHT}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -548,7 +556,7 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
private renderPublishStep(): string {
|
||||
return `
|
||||
<div class="publish-step">
|
||||
<button class="back-link" data-action="back-to-preview">\u2190 Back to preview</button>
|
||||
<button class="back-link" data-action="back-to-preview">\u2190 Back to press</button>
|
||||
<folk-pubs-publish-panel
|
||||
pdf-url="${this._pdfUrl}"
|
||||
format-id="${this._selectedFormat}"
|
||||
|
|
@ -747,6 +755,11 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
return;
|
||||
}
|
||||
|
||||
// Cache content before switching views (publish panel can't access textarea)
|
||||
this._cachedContent = content;
|
||||
this._cachedTitle = titleInput?.value?.trim() || "";
|
||||
this._cachedAuthor = authorInput?.value?.trim() || "";
|
||||
|
||||
this._loading = true;
|
||||
this._error = null;
|
||||
if (this._pdfUrl) URL.revokeObjectURL(this._pdfUrl);
|
||||
|
|
@ -764,8 +777,8 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
headers: { "Content-Type": "application/json", ...authHeaders },
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
title: titleInput?.value?.trim() || undefined,
|
||||
author: authorInput?.value?.trim() || undefined,
|
||||
title: this._cachedTitle || undefined,
|
||||
author: this._cachedAuthor || undefined,
|
||||
format: this._selectedFormat,
|
||||
}),
|
||||
});
|
||||
|
|
@ -784,7 +797,7 @@ export class FolkPubsEditor extends HTMLElement {
|
|||
this._pdfInfo = `${pageCount} pages \u00B7 ${format?.name || this._selectedFormat}`;
|
||||
this._loading = false;
|
||||
|
||||
// Auto-advance to preview
|
||||
// Auto-advance to press (preview) step
|
||||
this._view = "preview";
|
||||
this.render();
|
||||
} catch (e: any) {
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ export class FolkPubsFlipbook extends HTMLElement {
|
|||
|
||||
const maxW = Math.min((this.parentElement?.clientWidth || window.innerWidth) - 40, 700);
|
||||
const maxH = 500;
|
||||
let pageW = maxW / 2;
|
||||
let pageW = Math.max(maxW / 2, 100);
|
||||
let pageH = pageW / this._aspectRatio;
|
||||
if (pageH > maxH) {
|
||||
pageH = maxH;
|
||||
|
|
@ -151,7 +151,7 @@ export class FolkPubsFlipbook extends HTMLElement {
|
|||
<div class="reader">
|
||||
<div class="flipbook-row">
|
||||
<button class="nav-btn" data-dir="prev" title="Previous page">‹</button>
|
||||
<div class="flipbook-container" style="width:${pageW * 2}px; height:${pageH}px;"></div>
|
||||
<div class="flipbook-container" style="width:${Math.round(pageW * 2)}px; height:${Math.round(pageH)}px;"></div>
|
||||
<button class="nav-btn" data-dir="next" title="Next page">›</button>
|
||||
</div>
|
||||
<div class="page-info">
|
||||
|
|
@ -160,7 +160,10 @@ export class FolkPubsFlipbook extends HTMLElement {
|
|||
</div>
|
||||
`;
|
||||
|
||||
this.initFlipbook(pageW, pageH);
|
||||
this.initFlipbook(Math.round(pageW), Math.round(pageH)).catch((e) => {
|
||||
console.warn('[folk-pubs-flipbook] Flipbook init failed, using fallback:', e);
|
||||
this.renderFallback();
|
||||
});
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
|
|
@ -183,36 +186,69 @@ export class FolkPubsFlipbook extends HTMLElement {
|
|||
return;
|
||||
}
|
||||
|
||||
this._flipBook = new PageFlip(container, {
|
||||
width: Math.round(pageW),
|
||||
height: Math.round(pageH),
|
||||
showCover: true,
|
||||
maxShadowOpacity: 0.5,
|
||||
mobileScrollSupport: false,
|
||||
useMouseEvents: true,
|
||||
swipeDistance: 30,
|
||||
clickEventForward: false,
|
||||
flippingTime: 600,
|
||||
startPage: this._currentPage,
|
||||
});
|
||||
try {
|
||||
this._flipBook = new PageFlip(container, {
|
||||
width: pageW,
|
||||
height: pageH,
|
||||
showCover: true,
|
||||
maxShadowOpacity: 0.5,
|
||||
mobileScrollSupport: false,
|
||||
useMouseEvents: true,
|
||||
swipeDistance: 30,
|
||||
clickEventForward: false,
|
||||
flippingTime: 600,
|
||||
startPage: this._currentPage,
|
||||
});
|
||||
|
||||
const pages: HTMLElement[] = [];
|
||||
for (let i = 0; i < this._pageImages.length; i++) {
|
||||
const page = document.createElement("div");
|
||||
page.style.cssText = `
|
||||
width: 100%; height: 100%;
|
||||
background-image: url(${this._pageImages[i]});
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
`;
|
||||
pages.push(page);
|
||||
const pages: HTMLElement[] = [];
|
||||
for (let i = 0; i < this._pageImages.length; i++) {
|
||||
const page = document.createElement("div");
|
||||
page.className = "flipbook-page";
|
||||
page.style.cssText = `
|
||||
width: 100%; height: 100%;
|
||||
background-image: url("${this._pageImages[i]}");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-color: #fff;
|
||||
`;
|
||||
pages.push(page);
|
||||
}
|
||||
|
||||
this._flipBook.loadFromHTML(pages);
|
||||
this._flipBook.on("flip", (e: any) => {
|
||||
this._currentPage = e.data;
|
||||
this.updatePageInfo();
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[folk-pubs-flipbook] StPageFlip render failed, using fallback:', e);
|
||||
this._flipBook = null;
|
||||
this.renderFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
this._flipBook.loadFromHTML(pages);
|
||||
this._flipBook.on("flip", (e: any) => {
|
||||
this._currentPage = e.data;
|
||||
this.updatePageInfo();
|
||||
});
|
||||
// Verify pages actually rendered — if not, fall back after a short delay
|
||||
setTimeout(() => {
|
||||
if (!this.shadowRoot) return;
|
||||
const items = this.shadowRoot.querySelectorAll('.stf__item');
|
||||
// If StPageFlip created items but none are visible, fall back
|
||||
if (items.length > 0) {
|
||||
const anyVisible = Array.from(items).some(
|
||||
(el) => (el as HTMLElement).style.display !== 'none'
|
||||
);
|
||||
if (!anyVisible) {
|
||||
console.warn('[folk-pubs-flipbook] No visible pages after init, using fallback');
|
||||
this._flipBook?.destroy();
|
||||
this._flipBook = null;
|
||||
this.renderFallback();
|
||||
}
|
||||
} else if (!this.shadowRoot.querySelector('.stf__parent')) {
|
||||
// StPageFlip didn't create its structure at all
|
||||
console.warn('[folk-pubs-flipbook] StPageFlip structure missing, using fallback');
|
||||
this._flipBook = null;
|
||||
this.renderFallback();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Initial page info update for spread display
|
||||
this.updatePageInfo();
|
||||
}
|
||||
|
|
@ -272,9 +308,14 @@ export class FolkPubsFlipbook extends HTMLElement {
|
|||
${this.getStyles()}
|
||||
<div class="reader">
|
||||
<div class="fallback-pages">
|
||||
${this._pageImages.map((src, i) => `<img src="${src}" alt="Page ${i + 1}" />`).join('')}
|
||||
${this._pageImages.map((src, i) => `
|
||||
<div class="fallback-page-wrap">
|
||||
<span class="fallback-page-num">${i + 1}</span>
|
||||
<img src="${src}" alt="Page ${i + 1}" />
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="page-info">${this._numPages} pages</div>
|
||||
<div class="page-info">${this._numPages} pages (scroll view)</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -288,7 +329,7 @@ export class FolkPubsFlipbook extends HTMLElement {
|
|||
}
|
||||
|
||||
/* StPageFlip injects these into document.head, but they don't
|
||||
penetrate shadow DOM — so we replicate them here. */
|
||||
penetrate shadow DOM — replicate complete CSS here. */
|
||||
.stf__parent {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
|
@ -296,10 +337,15 @@ export class FolkPubsFlipbook extends HTMLElement {
|
|||
transform: translateZ(0);
|
||||
-ms-touch-action: pan-y;
|
||||
touch-action: pan-y;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.stf__wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.stf__parent canvas {
|
||||
|
|
@ -308,6 +354,7 @@ export class FolkPubsFlipbook extends HTMLElement {
|
|||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
.stf__block {
|
||||
position: absolute;
|
||||
|
|
@ -315,11 +362,17 @@ export class FolkPubsFlipbook extends HTMLElement {
|
|||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
perspective: 2000px;
|
||||
z-index: 1;
|
||||
}
|
||||
.stf__item {
|
||||
display: none;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
transform-style: preserve-3d;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.stf__outerShadow,
|
||||
.stf__innerShadow,
|
||||
|
|
@ -328,6 +381,11 @@ export class FolkPubsFlipbook extends HTMLElement {
|
|||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* Ensure page content fills the flipbook page */
|
||||
.flipbook-page {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
|
@ -361,6 +419,20 @@ export class FolkPubsFlipbook extends HTMLElement {
|
|||
display: flex; flex-direction: column; align-items: center; gap: 1rem;
|
||||
padding: 1rem; max-height: 500px; overflow-y: auto;
|
||||
}
|
||||
.fallback-page-wrap {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
}
|
||||
.fallback-page-num {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
font-size: 0.6rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
background: rgba(0,0,0,0.5);
|
||||
color: #fff;
|
||||
}
|
||||
.fallback-pages img {
|
||||
max-width: 100%; border-radius: 2px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
|
|
|
|||
|
|
@ -593,11 +593,23 @@ export class FolkPubsPublishPanel extends HTMLElement {
|
|||
}
|
||||
}
|
||||
|
||||
/** Get content from the parent editor by reading its textarea */
|
||||
/** Get content from the parent editor — uses cached values since textarea may not be in DOM during publish step */
|
||||
private getEditorContent(): { content: string; title?: string; author?: string } {
|
||||
const editor = this.closest("folk-pubs-editor") || document.querySelector("folk-pubs-editor");
|
||||
if (!editor?.shadowRoot) return { content: "" };
|
||||
if (!editor) return { content: "" };
|
||||
|
||||
// Try cached content first (always available after PDF generation)
|
||||
const cached = (editor as any).cachedContent;
|
||||
if (cached?.content) {
|
||||
return {
|
||||
content: cached.content,
|
||||
title: cached.title || undefined,
|
||||
author: cached.author || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback: try reading from shadow DOM (only works during write step)
|
||||
if (!editor.shadowRoot) return { content: "" };
|
||||
const textarea = editor.shadowRoot.querySelector(".content-area") as HTMLTextAreaElement;
|
||||
const titleInput = editor.shadowRoot.querySelector(".title-input") as HTMLInputElement;
|
||||
const authorInput = editor.shadowRoot.querySelector(".author-input") as HTMLInputElement;
|
||||
|
|
|
|||
|
|
@ -657,7 +657,24 @@ routes.get("/zine", (c) => {
|
|||
return c.redirect(`/${spaceSlug}?tool=folk-zine-gen`);
|
||||
});
|
||||
|
||||
// ── Page: Editor ──
|
||||
// ── Page: Editor (also served at /press) ──
|
||||
routes.get("/press", (c) => {
|
||||
const spaceSlug = c.req.param("space") || "personal";
|
||||
const dataSpace = c.get("effectiveSpace") || spaceSlug;
|
||||
return c.html(renderShell({
|
||||
title: `${spaceSlug} — rPubs Press | rSpace`,
|
||||
moduleId: "rpubs",
|
||||
spaceSlug,
|
||||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: `<folk-pubs-editor space="${spaceSlug}"></folk-pubs-editor>`,
|
||||
scripts: `<script type="module" src="/modules/rpubs/folk-pubs-editor.js?v=3"></script>
|
||||
<script type="module" src="/modules/rpubs/folk-pubs-flipbook.js?v=3"></script>
|
||||
<script type="module" src="/modules/rpubs/folk-pubs-publish-panel.js?v=3"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rpubs/pubs.css">`,
|
||||
}));
|
||||
});
|
||||
|
||||
routes.get("/", (c) => {
|
||||
const spaceSlug = c.req.param("space") || "personal";
|
||||
const dataSpace = c.get("effectiveSpace") || spaceSlug;
|
||||
|
|
@ -668,9 +685,9 @@ routes.get("/", (c) => {
|
|||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: `<folk-pubs-editor space="${spaceSlug}"></folk-pubs-editor>`,
|
||||
scripts: `<script type="module" src="/modules/rpubs/folk-pubs-editor.js?v=2"></script>
|
||||
<script type="module" src="/modules/rpubs/folk-pubs-flipbook.js"></script>
|
||||
<script type="module" src="/modules/rpubs/folk-pubs-publish-panel.js"></script>`,
|
||||
scripts: `<script type="module" src="/modules/rpubs/folk-pubs-editor.js?v=3"></script>
|
||||
<script type="module" src="/modules/rpubs/folk-pubs-flipbook.js?v=3"></script>
|
||||
<script type="module" src="/modules/rpubs/folk-pubs-publish-panel.js?v=3"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rpubs/pubs.css">`,
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue