diff --git a/next.config.mjs b/next.config.mjs index c4a93f3..0aa98dd 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -8,6 +8,9 @@ const nextConfig = { experimental: { // better-sqlite3 is a native module, keep it out of the webpack bundle serverComponentsExternalPackages: ["better-sqlite3"], + // don't reuse a cached dynamic page when navigating back, so the dashboard + // always reflects projects you just created or deleted + staleTimes: { dynamic: 0 }, }, }; diff --git a/src/app/editor/[id]/page.tsx b/src/app/editor/[id]/page.tsx index 2cec3ea..a3a0d3e 100644 --- a/src/app/editor/[id]/page.tsx +++ b/src/app/editor/[id]/page.tsx @@ -70,9 +70,9 @@ export default function EditorPage({ params }: { params: { id: string } }) { }; }, [id, compile]); - // open the log automatically when a compile fails + // open the log automatically when a compile fails or has errors useEffect(() => { - if (status === "error") setShowLog(true); + if (status === "error" || status === "warning") setShowLog(true); }, [status]); // flush pending work when leaving the editor diff --git a/src/app/globals.css b/src/app/globals.css index ea7f6b0..0f77a04 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -14,12 +14,13 @@ --color-text: 15 18 23; --color-text-muted: 92 99 112; - /* default accent is the LaTeX green (#50C878) */ - --color-accent: 80 200 120; + /* default accent is the LaTeX green (#14B84B) */ + --color-accent: 20 184 75; --color-accent-fg: 255 255 255; - --color-accent-soft: 224 245 233; + --color-accent-soft: 222 246 230; --color-danger: 225 76 76; + --color-warning: 217 119 6; --font-sans: "Inter", system-ui, sans-serif; --font-mono: "JetBrains Mono", ui-monospace, monospace; @@ -28,14 +29,14 @@ /* Per-engine accent. Set data-engine on a wrapper and everything tinted with the accent token follows along. Typst lands here in a later version. */ [data-engine="latex"] { - --color-accent: 80 200 120; - --color-accent-soft: 224 245 233; + --color-accent: 20 184 75; + --color-accent-soft: 222 246 230; } [data-engine="typst"] { - /* placeholder blue until Typst support arrives */ - --color-accent: 56 152 236; - --color-accent-soft: 222 238 252; + /* ready for when Typst support arrives */ + --color-accent: 0 122 255; + --color-accent-soft: 220 235 255; } /* Dark theme tokens live here once we ship dark mode. Toggling will be a matter diff --git a/src/components/dashboard/ProjectsGallery.tsx b/src/components/dashboard/ProjectsGallery.tsx index d9f4faa..233681e 100644 --- a/src/components/dashboard/ProjectsGallery.tsx +++ b/src/components/dashboard/ProjectsGallery.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { ConfirmDialog } from "@/components/ui/ConfirmDialog"; import type { Project } from "@/lib/projects"; import { ProjectCard } from "./ProjectCard"; @@ -26,6 +26,12 @@ export function ProjectsGallery({ const [showAll, setShowAll] = useState(false); const [pendingDelete, setPendingDelete] = useState(null); + // when the server sends a fresh list (e.g. after navigating back from a + // project), adopt it so deleted or newly created projects are reflected + useEffect(() => { + setProjects(initialProjects); + }, [initialProjects]); + async function togglePin(project: Project) { const pinned = !project.pinned; setProjects((prev) => diff --git a/src/components/editor/Toolbar.tsx b/src/components/editor/Toolbar.tsx index 8c9e0cf..2af0aac 100644 --- a/src/components/editor/Toolbar.tsx +++ b/src/components/editor/Toolbar.tsx @@ -36,6 +36,8 @@ function statusLabel( : "Compiled", className: "text-accent", }; + case "warning": + return { text: "Compiled with errors", className: "text-warning" }; case "error": return { text: "Compile errors", className: "text-danger" }; case "empty": diff --git a/src/components/editor/useCompile.ts b/src/components/editor/useCompile.ts index 607f2c3..3a10151 100644 --- a/src/components/editor/useCompile.ts +++ b/src/components/editor/useCompile.ts @@ -6,6 +6,8 @@ export type CompileStatus = | "idle" | "running" | "success" + // a PDF was produced but the log has errors (still worth showing the PDF) + | "warning" | "error" | "empty"; @@ -27,6 +29,7 @@ export function useCompile(projectId: string) { setLog(""); let success = false; + let pdfProduced = false; let empty = false; let duration: number | null = null; @@ -63,6 +66,7 @@ export function useCompile(projectId: string) { else if (event.type === "error") setLog((prev) => prev + event.message); else if (event.type === "done") { success = event.success; + pdfProduced = event.pdfProduced; empty = event.empty; duration = event.durationMs; } @@ -74,9 +78,10 @@ export function useCompile(projectId: string) { } setDurationMs(duration); - if (success) { - setStatus("success"); + // show the PDF whenever one was produced, even if the log has errors + if (pdfProduced) { setPdfVersion((v) => v + 1); + setStatus(success ? "success" : "warning"); } else if (empty) { setStatus("empty"); } else { diff --git a/src/lib/compiler.ts b/src/lib/compiler.ts index 7661f69..fa8502f 100644 --- a/src/lib/compiler.ts +++ b/src/lib/compiler.ts @@ -5,7 +5,11 @@ import { projectDir, projectOutputDir } from "./paths"; import { mainSourcePath, outputPdfPath, ensureProjectDirs } from "./storage"; export interface CompileResult { + // the compiler exited cleanly with a PDF success: boolean; + // a PDF came out even if there were errors. LaTeX often still produces one, + // and we'd rather show it (with the errors) than hide it, like Overleaf does. + pdfProduced: boolean; // the document compiled cleanly but had nothing to typeset (an empty body). // not a failure, just nothing to preview yet. empty: boolean; @@ -69,14 +73,28 @@ export async function runCompile( } catch { const message = `Unknown engine: ${engineId}\n`; onLog?.(message); - return { success: false, empty: false, log: message, durationMs: 0, passes: 0 }; + return { + success: false, + pdfProduced: 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, empty: false, log: message, durationMs: 0, passes: 0 }; + return { + success: false, + pdfProduced: false, + empty: false, + log: message, + durationMs: 0, + passes: 0, + }; } ensureProjectDirs(projectId); @@ -106,6 +124,7 @@ export async function runCompile( const empty = !pdfExists && /no pages of output/i.test(log); return { success: lastCode === 0 && pdfExists, + pdfProduced: pdfExists, empty, log, durationMs: Date.now() - start, @@ -138,6 +157,7 @@ export function compileStream( send({ type: "done", success: result.success, + pdfProduced: result.pdfProduced, empty: result.empty, durationMs: result.durationMs, passes: result.passes, diff --git a/tailwind.config.ts b/tailwind.config.ts index 05b865f..bf29a24 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -27,6 +27,7 @@ const config: Config = { soft: withAlpha("--color-accent-soft"), }, danger: withAlpha("--color-danger"), + warning: withAlpha("--color-warning"), }, fontFamily: { sans: ["var(--font-sans)", "system-ui", "sans-serif"],