From 0778c68ca1e1906c545d35988cc9666fedc8a6ee Mon Sep 17 00:00:00 2001 From: xarmian Date: Sat, 18 Apr 2026 15:50:27 -0400 Subject: [PATCH] feat(web): print header + footer for item detail pages (TASK-623) (#154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a rendered header and footer that repeat on every printed page. Replaces the browser's default print chrome (localhost URL, page title, date). Header (top of each page): {Workspace name} · {Collection icon + name} · {Issue ID} Footer (bottom of each page): Printed {date} · {full URL} · Page {n} Implementation - Two new `
`s rendered inside the item detail page template: `.print-header` and `.print-footer`. Both carry `aria-hidden` and are `display: none` on screen, so they never affect the live UI. - A `@media print` block shows them as `position: fixed` elements at top: 0 / bottom: 0. In Chromium this causes the browser to repeat them on every page of the print output. Firefox and Safari render them only on the first page -- documented as a known limitation since Chromium is the expected print target. - `@page { margin: 1.1in 0.6in 0.9in 0.6in }` carves out space for the header and footer strips so body content doesn't overlap them. This overrides the default 0.75in margin set in app.css (TASK-621). - Page number uses `.print-page-num::after { content: counter(page); }`. `counter(page)` in a pseudo-element evaluates to the current printed page number in all modern browsers. - Print date and URL are captured via a `beforeprint` listener so the values reflect the moment of print, not page-load time. Falls back to onMount values if `beforeprint` doesn't fire (older browsers). Users should uncheck "Headers and footers" in the browser print dialog for the cleanest result -- there's no CSS to suppress the browser's default print chrome. Parent: PLAN-620. --- .../[collection]/[slug]/+page.svelte | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/web/src/routes/[username]/[workspace]/[collection]/[slug]/+page.svelte b/web/src/routes/[username]/[workspace]/[collection]/[slug]/+page.svelte index eef33db4..fff6eabe 100644 --- a/web/src/routes/[username]/[workspace]/[collection]/[slug]/+page.svelte +++ b/web/src/routes/[username]/[workspace]/[collection]/[slug]/+page.svelte @@ -24,6 +24,7 @@ import { authStore } from '$lib/stores/auth.svelte'; import { starredStore } from '$lib/stores/starred.svelte'; import { titleStore } from '$lib/stores/title.svelte'; + import { workspaceStore } from '$lib/stores/workspace.svelte'; type RelationshipEntry = { key: string; @@ -118,8 +119,30 @@ // Sync coordinator — refresh item data on tab resume let unsubscribeSync: (() => void) | null = null; + let unsubscribeBeforePrint: (() => void) | null = null; + + // Print header/footer state (PLAN-620 / TASK-623). Initialized on mount + // and refreshed on `beforeprint` so the printed page reflects the + // actual moment of print, not whenever the page last loaded. + let printDate = $state(''); + let printUrl = $state(''); + + function refreshPrintMeta() { + if (typeof window === 'undefined') return; + printDate = new Date().toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + printUrl = window.location.href; + } onMount(() => { + refreshPrintMeta(); + const handler = () => refreshPrintMeta(); + window.addEventListener('beforeprint', handler); + unsubscribeBeforePrint = () => window.removeEventListener('beforeprint', handler); + unsubscribeSync = syncService.onSync(async (result) => { if (!wsSlug || !itemSlug || !item) return; // Don't refresh if the user is actively editing @@ -163,6 +186,7 @@ onDestroy(() => { unsubscribeSync?.(); + unsubscribeBeforePrint?.(); editorStore.resetForDoc(); collectionStore.setActiveItem(null); }); @@ -621,6 +645,22 @@ {:else if error}
{error}
{:else if item && collection} + + + +