From c9b66c30192e827c8b716a0f5f8f34ecd197fda6 Mon Sep 17 00:00:00 2001 From: ThomasBrenas <246412079+ThomasBrenas@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:57:16 +1000 Subject: [PATCH] Add a Print / PDF export button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The @media print stylesheet already reduces a solved train to a clean spec sheet — summary line, best-row schematic, and results table, with the input panel, onboarding intro, and export controls hidden. There was no affordance for it, so a user had to reach for the browser's own print command. Add a "Print / PDF" button alongside Copy link / Copy JSON / Download CSV that calls window.print(); the browser dialog covers "Save as PDF" for free. The button carries data-export, so the existing print CSS hides it in the output. Closes #3 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LegpdV1tMs2dUyzAd7tCzH --- packages/web/src/ui/app.test.ts | 15 ++++++++++++++- packages/web/src/ui/app.ts | 4 ++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/web/src/ui/app.test.ts b/packages/web/src/ui/app.test.ts index 006541f..4a9315e 100644 --- a/packages/web/src/ui/app.test.ts +++ b/packages/web/src/ui/app.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; import { mountApp } from "./app.js"; describe("app", () => { @@ -42,6 +42,19 @@ describe("app", () => { history.replaceState(null, "", "#"); }); + it("offers a Print / PDF button that opens the browser print dialog", () => { + const root = document.createElement("main"); + mountApp(root); + const print = root.querySelector('[data-export="print"]')!; + expect(print).not.toBeNull(); + expect(print.textContent).toBe("Print / PDF"); + const spy = vi.spyOn(window, "print").mockImplementation(() => {}); + print.click(); + expect(spy).toHaveBeenCalledTimes(1); + spy.mockRestore(); + history.replaceState(null, "", "#"); + }); + it("shows a readable notice for malformed input instead of throwing", () => { const root = document.createElement("main"); mountApp(root); diff --git a/packages/web/src/ui/app.ts b/packages/web/src/ui/app.ts index 9f5b531..dbfe7d2 100644 --- a/packages/web/src/ui/app.ts +++ b/packages/web/src/ui/app.ts @@ -32,6 +32,7 @@ export function mountApp(root: HTMLElement): void { + `; let rows: ResultRow[] = []; let summary: SolveSummary | undefined; @@ -100,6 +101,9 @@ export function mountApp(root: HTMLElement): void { const blob = new Blob([toCsv(rows, summary)], { type: "text/csv" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "involute-frontier.csv"; a.click(); }); + // The @media print stylesheet already reduces the page to a spec sheet + // (schematic + table + summary), so opening the browser dialog is all we need. + root.querySelector('[data-export="print"]')!.addEventListener("click", () => window.print()); // initial solve with the panel's default request panel.dispatchEvent(new Event("change", { bubbles: true })); }