Print-friendly view for single recipes
Adds a "Print view" button on each recipe that opens a clean black-on-white page showing only that recipe, with tick boxes next to ingredients and numbered steps. You can print it or save it as a PDF, then go back with a button or the Esc key.
What it does
- In the recipe detail view, replace the "🖨 Print" button with a "🖨 Print view" button (tooltip: "Show a clean, printer-friendly version of this recipe"). Clicking it navigates to a new "print" view for that recipe instead of calling window.print() directly.
- Print view layout: a toolbar at top with a "← Back to recipe" button (returns to the detail view), a hint text "This is how your recipe will look on paper.", and a primary "🖨 Print" button that calls window.print().
- Below the toolbar, show a paper-like sheet (max width 8.5in, white background, black text, serif font such as Georgia, 12pt, subtle shadow on screen) containing: recipe title as H1; description in italics (if present); a meta line with "Serves: …" and "Time: …" (each only if set); tags joined with " · " (if any); an "Ingredients" heading followed by a two-column list (one column on narrow screens) where each item has a small empty square checkbox drawn before it; a "Steps" heading followed by a numbered list; and a footer line "From my Recipe Box" right-aligned.
- If a recipe has no ingredients or steps, show "No ingredients listed." or "No steps listed." in italics.
- In print view, hide the site header and use a light grey page background on screen. Headings avoid page breaks right after them, and individual ingredients and steps avoid splitting across pages.
- When printing: hide the header, action buttons, toolbar and anything marked no-print (including the storage warning banner); use a white background, 15mm page margins, and remove the sheet's shadow, padding and max-width and the main container's padding.
- While in print view, set the browser tab/document title to "<recipe title> – Recipe Box" (so saved PDFs are named after the recipe); otherwise the title is "Recipe Box".
- Pressing Escape while in print view returns to the recipe detail view.
- Update the README to replace the "Edit, delete or print" bullet with "Edit or delete" and add a "Print" bullet describing the print view, the Print button, and Back/Esc.
Safety check ✓ Safe
The changes only affect page layout, styling and a view switch in the single recipe page, plus a README update. There are no new dependencies, network requests, or data access beyond the recipe already being shown.
Conversation that created this patch
@priya
Add a print-friendly view for a single recipe.
AI
I've added a print-friendly page for each recipe.
**How it works:** Open any recipe and click the **🖨 Print view** button (it used to say just "Print"). The recipe then shows up as a clean white page, the way it will look on paper:
- The menus, search bar and buttons are gone.
- The text is black on white, so it's easy to read in the kitchen.
- Ingredients are in two columns, each with a small tick box you can mark with a pen.
- Steps are numbered.
Click **🖨 Print** to print it, or to save it as a PDF from your browser's print window. Click **← Back to recipe** or press Esc to go back.
**Good to know:**
- The printout is labelled with the recipe's name, so a saved PDF will usually be named after the recipe too.
- Where it can, the page avoids splitting a single step or ingredient across two sheets.
- I also updated the instructions file to describe the new print view.
I couldn't open a real browser here, so I couldn't check how the page actually looks or prints. I did run a test that clicked through to the print view, pressed Print, and returned with Esc, and all of that worked.
Code changes (for the curious, 2 files) · raw
README.md +5 −1
@@ -24,7 +24,11 @@ That's it. | |
descriptions, tags, ingredients and steps. | |
- **Filter by tag**: click a tag (like "dinner" or "vegetarian") above the list. | |
- **While cooking**: tick off ingredients as you gather them. | |
| − | - **Edit, delete or print** a recipe using the buttons at the top of the recipe. |
| + | - **Edit or delete** a recipe using the buttons at the top of the recipe. |
| + | - **Print**: open a recipe and click **🖨 Print view**. You'll see a clean, |
| + | black-and-white page with just the recipe (no buttons or menus), with little |
| + | tick boxes next to each ingredient. Click **🖨 Print** to print it or save it |
| + | as a PDF, and **← Back to recipe** (or press Esc) to return. |
| |
## Where are my recipes saved? | |
|
index.html +99 −5
@@ -146,10 +146,59 @@ | |
background: #fff4d6; border: 1px solid #f0d58a; border-radius: var(--radius); | |
padding: 10px 14px; margin-bottom: 16px; font-size: 0.95rem; | |
} | |
| + | /* ---------- Print-friendly view ---------- */ |
| + | body.print-mode { background: #e9e6e1; } |
| + | body.print-mode header { display: none; } |
| + | .print-toolbar { |
| + | max-width: 8.5in; margin: 0 auto 16px; |
| + | display: flex; gap: 8px; align-items: center; flex-wrap: wrap; |
| + | } |
| + | .print-toolbar .spacer { flex: 1; } |
| + | .print-toolbar .hint { color: var(--muted); font-size: 0.9rem; } |
| + | .sheet { |
| + | max-width: 8.5in; margin: 0 auto; |
| + | background: #fff; color: #000; |
| + | padding: 0.8in 0.9in; |
| + | box-shadow: 0 2px 12px rgba(0,0,0,.15); |
| + | font-family: Georgia, "Times New Roman", serif; |
| + | font-size: 12pt; line-height: 1.45; |
| + | } |
| + | @media (max-width: 700px) { .sheet { padding: 24px 20px; } } |
| + | .sheet h1 { font-size: 22pt; margin: 0 0 4px; line-height: 1.2; } |
| + | .sheet .p-desc { font-style: italic; margin: 0 0 8px; } |
| + | .sheet .p-meta { display: flex; flex-wrap: wrap; gap: 4px 20px; font-size: 10.5pt; margin: 0 0 4px; } |
| + | .sheet .p-meta b { font-weight: 700; } |
| + | .sheet .p-tags { font-size: 10pt; color: #444; margin: 0; } |
| + | .sheet h2 { |
| + | font-size: 13pt; text-transform: uppercase; letter-spacing: .06em; |
| + | border-bottom: 1.5px solid #000; padding-bottom: 3px; margin: 22px 0 10px; |
| + | break-after: avoid; page-break-after: avoid; |
| + | } |
| + | .sheet ul.p-ingredients { list-style: none; padding: 0; margin: 0; columns: 2; column-gap: 36px; } |
| + | @media (max-width: 600px) { .sheet ul.p-ingredients { columns: 1; } } |
| + | .sheet ul.p-ingredients li { |
| + | display: flex; gap: 8px; padding: 2px 0; |
| + | break-inside: avoid; page-break-inside: avoid; |
| + | } |
| + | .sheet ul.p-ingredients li::before { |
| + | content: ""; flex: 0 0 auto; width: 10px; height: 10px; |
| + | border: 1px solid #000; margin-top: 0.42em; |
| + | } |
| + | .sheet ol.p-steps { padding-left: 24px; margin: 0; } |
| + | .sheet ol.p-steps li { margin-bottom: 8px; padding-left: 4px; break-inside: avoid; page-break-inside: avoid; } |
| + | .sheet .p-empty { font-style: italic; color: #444; margin: 0; } |
| + | .sheet .p-footer { |
| + | margin-top: 28px; padding-top: 6px; border-top: 1px solid #bbb; |
| + | font-size: 9pt; color: #555; text-align: right; |
| + | } |
| + | |
@media print { | |
| − | header, .actions, .no-print { display: none !important; } |
| − | body { background: #fff; } |
| + | @page { margin: 15mm; } |
| + | header, .actions, .no-print, .print-toolbar { display: none !important; } |
| + | body, body.print-mode { background: #fff; } |
| + | main { padding: 0; max-width: none; } |
.panel { border: none; padding: 0; } | |
| + | .sheet { box-shadow: none; padding: 0; margin: 0; max-width: none; } |
} | |
</style> | |
</head> | |
@@ -338,11 +387,15 @@ | |
function render() { | |
app.innerHTML = ""; | |
if (!storageOk) { | |
| − | app.appendChild(el("div", { class: "warning", text: |
| + | app.appendChild(el("div", { class: "warning no-print", text: |
"Heads up: your browser isn't letting this page save data, so changes will be lost when you close it. " + | |
"Try opening the file in a regular (not private) window or a different browser." })); | |
} | |
| − | if (state.view === "detail" && find(state.id)) renderDetail(find(state.id)); |
| + | var printing = state.view === "print" && !!find(state.id); |
| + | document.body.classList.toggle("print-mode", printing); |
| + | document.title = printing ? find(state.id).title + " – Recipe Box" : "Recipe Box"; |
| + | if (printing) renderPrint(find(state.id)); |
| + | else if (state.view === "detail" && find(state.id)) renderDetail(find(state.id)); |
else if (state.view === "form") renderForm(state.id ? find(state.id) : null); | |
else { state.view = "list"; renderList(); } | |
} | |
@@ -435,7 +488,7 @@ | |
var actions = el("div", { class: "actions" }, [ | |
el("button", { text: "← All recipes", onclick: function () { go("list"); } }), | |
el("span", { class: "spacer" }), | |
| − | el("button", { text: "🖨 Print", onclick: function () { window.print(); } }), |
| + | el("button", { text: "🖨 Print view", title: "Show a clean, printer-friendly version of this recipe", onclick: function () { go("print", r.id); } }), |
el("button", { text: "✏️ Edit", onclick: function () { go("form", r.id); } }), | |
el("button", { class: "danger", text: "🗑 Delete", onclick: function () { | |
if (confirm('Delete "' + r.title + '"? This can\'t be undone.')) { | |
@@ -480,6 +533,42 @@ | |
])); | |
} | |
| |
| + | // ---------- Print-friendly view ---------- |
| + | function renderPrint(r) { |
| + | var toolbar = el("div", { class: "print-toolbar" }, [ |
| + | el("button", { text: "← Back to recipe", onclick: function () { go("detail", r.id); } }), |
| + | el("span", { class: "spacer" }), |
| + | el("span", { class: "hint", text: "This is how your recipe will look on paper." }), |
| + | el("button", { class: "primary", text: "🖨 Print", onclick: function () { window.print(); } }) |
| + | ]); |
| + | |
| + | var meta = []; |
| + | if (r.servings) meta.push(el("span", null, [el("b", { text: "Serves: " }), r.servings])); |
| + | if (r.time) meta.push(el("span", null, [el("b", { text: "Time: " }), r.time])); |
| + | |
| + | var ingredients = r.ingredients || []; |
| + | var stepsList = r.steps || []; |
| + | |
| + | var sheet = el("article", { class: "sheet" }, [ |
| + | el("h1", { text: r.title }), |
| + | r.description ? el("p", { class: "p-desc", text: r.description }) : null, |
| + | meta.length ? el("div", { class: "p-meta" }, meta) : null, |
| + | (r.tags && r.tags.length) ? el("p", { class: "p-tags", text: r.tags.join(" · ") }) : null, |
| + | el("h2", { text: "Ingredients" }), |
| + | ingredients.length |
| + | ? el("ul", { class: "p-ingredients" }, ingredients.map(function (i) { return el("li", { text: i }); })) |
| + | : el("p", { class: "p-empty", text: "No ingredients listed." }), |
| + | el("h2", { text: "Steps" }), |
| + | stepsList.length |
| + | ? el("ol", { class: "p-steps" }, stepsList.map(function (s) { return el("li", { text: s }); })) |
| + | : el("p", { class: "p-empty", text: "No steps listed." }), |
| + | el("div", { class: "p-footer", text: "From my Recipe Box" }) |
| + | ]); |
| + | |
| + | app.appendChild(toolbar); |
| + | app.appendChild(sheet); |
| + | } |
| + | |
// ---------- Add / edit form ---------- | |
function renderForm(r) { | |
var editing = !!r; | |
@@ -575,6 +664,11 @@ | |
render(); | |
}); | |
| |
| + | // Esc leaves the print view |
| + | document.addEventListener("keydown", function (e) { |
| + | if (e.key === "Escape" && state.view === "print") go("detail", state.id); |
| + | }); |
| + | |
// Keep multiple open tabs in sync | |
window.addEventListener("storage", function (e) { | |
if (e.key === STORAGE_KEY) { |