diff --git a/modules/rpubs/components/folk-pubs-editor.ts b/modules/rpubs/components/folk-pubs-editor.ts index 38cc606..3b41e3f 100644 --- a/modules/rpubs/components/folk-pubs-editor.ts +++ b/modules/rpubs/components/folk-pubs-editor.ts @@ -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 {
${this._pdfInfo ? `${this._pdfInfo}` : ''} @@ -515,8 +523,8 @@ export class FolkPubsEditor extends HTMLElement { ${this._error ? `
${this.escapeHtml(this._error)}
` : ''} @@ -539,7 +547,7 @@ export class FolkPubsEditor extends HTMLElement {
${this._pdfInfo || ''} - +
`; @@ -548,7 +556,7 @@ export class FolkPubsEditor extends HTMLElement { private renderPublishStep(): string { return `
- + maxH) { pageH = maxH; @@ -151,7 +151,7 @@ export class FolkPubsFlipbook extends HTMLElement {
-
+
@@ -160,7 +160,10 @@ export class FolkPubsFlipbook extends HTMLElement {
`; - 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()}
- ${this._pageImages.map((src, i) => `Page ${i + 1}`).join('')} + ${this._pageImages.map((src, i) => ` +
+ ${i + 1} + Page ${i + 1} +
+ `).join('')}
-
${this._numPages} pages
+
${this._numPages} pages (scroll view)
`; } @@ -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); diff --git a/modules/rpubs/components/folk-pubs-publish-panel.ts b/modules/rpubs/components/folk-pubs-publish-panel.ts index 849d345..ecd19dc 100644 --- a/modules/rpubs/components/folk-pubs-publish-panel.ts +++ b/modules/rpubs/components/folk-pubs-publish-panel.ts @@ -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; diff --git a/modules/rpubs/mod.ts b/modules/rpubs/mod.ts index 63a3dd7..117287d 100644 --- a/modules/rpubs/mod.ts +++ b/modules/rpubs/mod.ts @@ -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: ``, + scripts: ` + + `, + styles: ``, + })); +}); + 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: ``, - scripts: ` - - `, + scripts: ` + + `, styles: ``, })); });