diff --git a/README.md b/README.md index a0b1f67..34feb9e 100644 --- a/README.md +++ b/README.md @@ -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? diff --git a/index.html b/index.html index 93857b2..540f2de 100644 --- a/index.html +++ b/index.html @@ -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; } } @@ -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) {