diff --git a/src/app/editor/[id]/page.tsx b/src/app/editor/[id]/page.tsx index 6daea60..2cec3ea 100644 --- a/src/app/editor/[id]/page.tsx +++ b/src/app/editor/[id]/page.tsx @@ -162,7 +162,13 @@ export default function EditorPage({ params }: { params: { id: string } }) { )} } - right={} + right={ + + } /> diff --git a/src/app/page.tsx b/src/app/page.tsx index aec4934..ff16e16 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,3 @@ -import Image from "next/image"; import { listProjects } from "@/lib/projects"; import { templates } from "@/lib/templates"; import { NewProjectButton } from "@/components/dashboard/NewProjectButton"; @@ -21,9 +20,8 @@ export default function DashboardPage() { return (
-
- - TexSet +
+ TexSet
diff --git a/src/components/editor/PreviewPane.tsx b/src/components/editor/PreviewPane.tsx index 656f334..320eca6 100644 --- a/src/components/editor/PreviewPane.tsx +++ b/src/components/editor/PreviewPane.tsx @@ -1,21 +1,28 @@ "use client"; import { useEffect, useRef, useState } from "react"; +import type { CompileStatus } from "./useCompile"; -type PreviewState = "loading" | "ready" | "empty" | "error"; +type RenderState = "loading" | "ready" | "blank" | "error"; interface PreviewPaneProps { projectId: string; // bumped after each successful compile so we know to re-render the PDF version: number; + // the compile outcome, used to explain why there's nothing to show yet + documentStatus: CompileStatus; } // Renders the compiled PDF with pdf.js, one canvas per page. The worker is // bundled from the package so it keeps working offline. -export function PreviewPane({ projectId, version }: PreviewPaneProps) { +export function PreviewPane({ + projectId, + version, + documentStatus, +}: PreviewPaneProps) { const pagesRef = useRef(null); const scrollRef = useRef(null); - const [state, setState] = useState("loading"); + const [state, setState] = useState("blank"); useEffect(() => { let cancelled = false; @@ -26,9 +33,9 @@ export function PreviewPane({ projectId, version }: PreviewPaneProps) { const scroller = scrollRef.current; if (!container || !scroller) return; - // nothing has been compiled yet on first open + // no successful compile yet, so there's no PDF to render if (version === 0) { - setState("empty"); + setState("blank"); return; } @@ -92,24 +99,30 @@ export function PreviewPane({ projectId, version }: PreviewPaneProps) { }; }, [projectId, version]); + // pick the placeholder message for when there's no rendered PDF on screen + const placeholder = (() => { + if (documentStatus === "running") return null; // show the spinner instead + if (documentStatus === "empty") + return "This document is empty. Start writing to see the preview."; + if (documentStatus === "error" || state === "error") + return "No preview yet. Check the compile log for errors."; + return "Compile to see the preview."; + })(); + + const showOverlay = state !== "ready"; + const showSpinner = + state === "loading" || (state === "blank" && documentStatus === "running"); + return (
- {state !== "ready" && ( -
- {state === "loading" && ( + {showOverlay && ( +
+ {showSpinner ? ( - )} - {state === "empty" && ( -

- Compile to see the preview. -

- )} - {state === "error" && ( -

- No preview yet. Check the compile log for errors. -

+ ) : ( +

{placeholder}

)}
)} diff --git a/src/components/editor/Toolbar.tsx b/src/components/editor/Toolbar.tsx index 9177138..8c9e0cf 100644 --- a/src/components/editor/Toolbar.tsx +++ b/src/components/editor/Toolbar.tsx @@ -38,6 +38,8 @@ function statusLabel( }; case "error": return { text: "Compile errors", className: "text-danger" }; + case "empty": + return { text: "Empty document", className: "text-text-muted" }; default: return { text: "", className: "" }; } diff --git a/src/components/editor/useCompile.ts b/src/components/editor/useCompile.ts index f8d249c..607f2c3 100644 --- a/src/components/editor/useCompile.ts +++ b/src/components/editor/useCompile.ts @@ -2,7 +2,12 @@ import { useCallback, useRef, useState } from "react"; -export type CompileStatus = "idle" | "running" | "success" | "error"; +export type CompileStatus = + | "idle" + | "running" + | "success" + | "error" + | "empty"; // Drives compilation for one project. Reads the Server-Sent Events stream from // the compile route, accumulating the log and learning when the PDF is ready. @@ -22,6 +27,7 @@ export function useCompile(projectId: string) { setLog(""); let success = false; + let empty = false; let duration: number | null = null; try { @@ -57,6 +63,7 @@ export function useCompile(projectId: string) { else if (event.type === "error") setLog((prev) => prev + event.message); else if (event.type === "done") { success = event.success; + empty = event.empty; duration = event.durationMs; } } @@ -70,6 +77,8 @@ export function useCompile(projectId: string) { if (success) { setStatus("success"); setPdfVersion((v) => v + 1); + } else if (empty) { + setStatus("empty"); } else { setStatus("error"); } diff --git a/src/lib/compiler.ts b/src/lib/compiler.ts index 3a61642..7661f69 100644 --- a/src/lib/compiler.ts +++ b/src/lib/compiler.ts @@ -6,6 +6,9 @@ import { mainSourcePath, outputPdfPath, ensureProjectDirs } from "./storage"; export interface CompileResult { success: boolean; + // the document compiled cleanly but had nothing to typeset (an empty body). + // not a failure, just nothing to preview yet. + empty: boolean; log: string; durationMs: number; passes: number; @@ -66,14 +69,14 @@ export async function runCompile( } catch { const message = `Unknown engine: ${engineId}\n`; onLog?.(message); - return { success: false, log: message, durationMs: 0, passes: 0 }; + return { success: false, empty: false, log: message, durationMs: 0, passes: 0 }; } const mainPath = mainSourcePath(projectId, engine); if (!fs.existsSync(mainPath)) { const message = `No ${engine.mainFileName} found for this project.\n`; onLog?.(message); - return { success: false, log: message, durationMs: 0, passes: 0 }; + return { success: false, empty: false, log: message, durationMs: 0, passes: 0 }; } ensureProjectDirs(projectId); @@ -99,8 +102,11 @@ export async function runCompile( } const pdfExists = fs.existsSync(outputPdfPath(projectId, engine)); + // an empty body makes xelatex finish cleanly with no PDF and this notice + const empty = !pdfExists && /no pages of output/i.test(log); return { success: lastCode === 0 && pdfExists, + empty, log, durationMs: Date.now() - start, passes, @@ -132,6 +138,7 @@ export function compileStream( send({ type: "done", success: result.success, + empty: result.empty, durationMs: result.durationMs, passes: result.passes, });