From af073f87daba14119bb987a654e50fbdafc3e3b3 Mon Sep 17 00:00:00 2001 From: mAi Date: Thu, 21 May 2026 22:01:46 +0200 Subject: [PATCH] fix(print): default to portrait, opt-in landscape for wide surfaces (t-paliad-233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The smart-timeline-chart block in global.css declared @page { size: A4 landscape } inside @media print. @page rules are global even when nested in selectors, so this leaked landscape onto every printed surface in paliad — not just the chart. Switch to named-page strategy: - Default @page { size: A4 portrait; margin: 1.5cm 1.2cm } - @page paliad-landscape { size: A4 landscape; margin: 1.5cm } - @media print: body. { page: paliad-landscape } opts surfaces that need width into landscape via per-page body classes Landscape opt-ins: - body.page-kostenrechner — wide fee-tier tables - body.page-projects-chart — horizontal Smart Timeline chart - body.events-view-calendar — /events Kalender tab (month grid) - body.views-shape-active-calendar / -timeline — Custom Views shapes - body.verfahrensablauf-view-timeline — horizontal procedure timeline Body classes: - kostenrechner.tsx, projects-chart.tsx, verfahrensablauf.tsx now set page- on body - verfahrensablauf.ts toggles verfahrensablauf-view-(timeline|columns) in initViewToggle - views.ts toggles views-shape-active- in setActiveShape (mirrors the existing events.ts events-view-* pattern) General print polish in the universal block (the catch-all at the bottom of global.css): - Hide .fab / .fab-button / .edit-mode-handle / .paliadin-widget / [data-print-hide] in print - thead { display: table-header-group } so headers repeat across pages - tr/th/td page-break-inside: avoid so rows don't split mid-cell - h1-h6 page-break-after: avoid, orphans/widows: 3 for p/h*/li - print-color-adjust: exact on brand-coloured headers + status pills - a[href^="http"]::after content: " (" attr(href) ")" prints external URLs after their link text (opt-out via data-print-url="hide") - body font-size: 11pt for print readability Verified via Playwright on static dist build that: - Default surfaces (dashboard, projects, fristenrechner, agenda, admin) match no page: rule → portrait - kostenrechner, projects-chart match the landscape rule - verfahrensablauf-view-columns → portrait, -view-timeline → landscape - views-shape-active-list/-cards → portrait, -calendar/-timeline → landscape - /events default (events-view-cards) → portrait, calendar toggle → landscape go build ./... + go test ./internal/... + bun test (99 pass) + bun run build all clean. --- frontend/src/client/verfahrensablauf.ts | 12 +++ frontend/src/client/views.ts | 6 ++ frontend/src/kostenrechner.tsx | 2 +- frontend/src/projects-chart.tsx | 2 +- frontend/src/styles/global.css | 103 +++++++++++++++++++++--- frontend/src/verfahrensablauf.tsx | 2 +- 6 files changed, 111 insertions(+), 16 deletions(-) diff --git a/frontend/src/client/verfahrensablauf.ts b/frontend/src/client/verfahrensablauf.ts index 7b4b2cd..8bee000 100644 --- a/frontend/src/client/verfahrensablauf.ts +++ b/frontend/src/client/verfahrensablauf.ts @@ -283,18 +283,30 @@ function selectProceeding(btn: HTMLButtonElement) { scheduleCalc(0); } +function applyVerfahrensablaufViewBodyClass(view: ProcedureView) { + // Mirrors the events.ts pattern (body.events-view-*). The print + // stylesheet keys `body.verfahrensablauf-view-timeline` to + // `@page paliad-landscape`, so flipping this class is what lets a + // user print the horizontal timeline in landscape without affecting + // the columns view (which stays portrait). + document.body.classList.toggle("verfahrensablauf-view-timeline", view === "timeline"); + document.body.classList.toggle("verfahrensablauf-view-columns", view === "columns"); +} + function initViewToggle() { const toggle = document.getElementById("fristen-view-toggle"); if (!toggle) return; const initial = new URLSearchParams(window.location.search).get("view"); if (initial === "timeline") procedureView = "timeline"; + applyVerfahrensablaufViewBodyClass(procedureView); toggle.querySelectorAll("input[name=fristen-view]").forEach((input) => { input.checked = input.value === procedureView; input.addEventListener("change", () => { if (!input.checked) return; procedureView = input.value === "columns" ? "columns" : "timeline"; + applyVerfahrensablaufViewBodyClass(procedureView); const url = new URL(window.location.href); if (procedureView === "columns") { url.searchParams.delete("view"); diff --git a/frontend/src/client/views.ts b/frontend/src/client/views.ts index fd6f818..fe37dc0 100644 --- a/frontend/src/client/views.ts +++ b/frontend/src/client/views.ts @@ -204,6 +204,12 @@ function setActiveShape(shape: RenderShape | null): void { document.querySelectorAll("#views-shape-chips [data-shape]").forEach((btn) => { btn.classList.toggle("active", btn.dataset.shape === shape); }); + // Mirror the active shape on so the print stylesheet can opt + // calendar / timeline into landscape (`@page paliad-landscape`) while + // list / cards stay portrait — t-paliad-233. + for (const s of ["list", "cards", "calendar", "timeline"]) { + document.body.classList.toggle(`views-shape-active-${s}`, shape === s); + } } let timelineHandle: ChartHandle | null = null; diff --git a/frontend/src/kostenrechner.tsx b/frontend/src/kostenrechner.tsx index 99368b9..7471470 100644 --- a/frontend/src/kostenrechner.tsx +++ b/frontend/src/kostenrechner.tsx @@ -104,7 +104,7 @@ export function renderKostenrechner(): string { Prozesskostenrechner — Paliad - + diff --git a/frontend/src/projects-chart.tsx b/frontend/src/projects-chart.tsx index de66c25..c4ed349 100644 --- a/frontend/src/projects-chart.tsx +++ b/frontend/src/projects-chart.tsx @@ -27,7 +27,7 @@ export function renderProjectsChart(): string { Projekt-Chart — Paliad - + diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 085f3b0..c2384ce 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -4902,11 +4902,6 @@ dialog.modal::backdrop { font-size: 11pt; color: #000; } - - @page { - margin: 2cm; - size: A4; - } } /* --- Geb\u00fchrentabellen --- */ @@ -12185,8 +12180,38 @@ dialog.quick-add-sheet::backdrop { gericht cards, entity tables, glossar entries, checklist items) print as-is. Per-page print rules above this block handle their specific tweaks; this block is the catch-all for chrome that those rules miss. + + Orientation strategy (t-paliad-233): + - Default `@page` is A4 portrait. The CSS `@page` rule is *global* + even when nested inside `@media print` — so prior to t-paliad-233 + a stray `@page { size: A4 landscape }` in the smart-timeline-chart + block was leaking landscape onto every printed surface. + - Surfaces that genuinely need width opt into the named + `paliad-landscape` page via a `page: paliad-landscape` declaration + on a per-page body class. Wired below: Kostenrechner, projects + chart, Custom Views calendar / timeline, /events Kalender, + Verfahrensablauf timeline view. ============================================================================ */ +@page { + size: A4 portrait; + margin: 1.5cm 1.2cm; +} + +@page paliad-landscape { + size: A4 landscape; + margin: 1.5cm; +} + @media print { + body.page-kostenrechner, + body.page-projects-chart, + body.events-view-calendar, + body.views-shape-active-calendar, + body.views-shape-active-timeline, + body.verfahrensablauf-view-timeline { + page: paliad-landscape; + } + .header, .footer, .sidebar, @@ -12222,6 +12247,11 @@ dialog.quick-add-sheet::backdrop { .event-picker-row, .date-input-group, .wizard-step-hint, + .fab, + .fab-button, + .edit-mode-handle, + .paliadin-widget, + [data-print-hide], #step-1, #step-2, #event-step-1, @@ -12274,14 +12304,64 @@ dialog.quick-add-sheet::backdrop { break-inside: avoid; } + /* Tables: repeat headers on each printed page, keep rows intact. */ + thead { + display: table-header-group; + } + + tfoot { + display: table-footer-group; + } + + tr, + th, + td { + page-break-inside: avoid; + break-inside: avoid; + } + + /* Orphans / widows defensive defaults. */ + p, h1, h2, h3, h4, h5, h6, li { + orphans: 3; + widows: 3; + } + + h1, h2, h3, h4, h5, h6 { + page-break-after: avoid; + break-after: avoid; + } + body { background: #fff !important; color: #000 !important; + font-size: 11pt; } - @page { - size: A4; - margin: 1.5cm; + /* Brand-coloured headers and status pills keep their fill in print + instead of losing background colour to the default print bleach. */ + .print-header, + .checklist-regime, + .gebuehren-table th, + .entity-status-pill, + .fr-col-header, + [data-print-color="exact"] { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + + /* External hyperlinks: print the URL after the link text so the + printed page remains traceable. Skip same-page fragment anchors + and javascript: pseudo-links; skip links whose text already *is* + the URL (avoids duplicates like "https://… (https://…)"). */ + a[href^="http"]:not([href*="#"])::after { + content: " (" attr(href) ")"; + font-size: 9pt; + color: #555; + word-break: break-all; + } + + a[href^="http"][data-print-url="hide"]::after { + content: none; } } @@ -16066,13 +16146,10 @@ dialog.quick-add-sheet::backdrop { - Force the print palette regardless of the user's screen choice (B&W shows nothing the user didn't intend, redactable). - Hide chrome (sidebar, footer, header, bottom-nav, control chips). - - Let the chart fill landscape A4 width. + - Let the chart fill landscape A4 width via the named `paliad-landscape` + page declared in the universal print block (t-paliad-233). - Add a printed header with project meta on the chart page. */ @media print { - @page { - size: A4 landscape; - margin: 1.5cm; - } body.has-sidebar > aside.sidebar, body.has-sidebar > .bottom-nav, body.has-sidebar > footer, diff --git a/frontend/src/verfahrensablauf.tsx b/frontend/src/verfahrensablauf.tsx index d3a0dd1..f1a3bf1 100644 --- a/frontend/src/verfahrensablauf.tsx +++ b/frontend/src/verfahrensablauf.tsx @@ -84,7 +84,7 @@ export function renderVerfahrensablauf(): string { Verfahrensablauf — Paliad - +