fix(print): default to portrait, opt-in landscape for wide surfaces (t-paliad-233)

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.<surface> { 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-<slug> on body
- verfahrensablauf.ts toggles verfahrensablauf-view-(timeline|columns)
  in initViewToggle
- views.ts toggles views-shape-active-<shape> 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.
This commit is contained in:
mAi
2026-05-21 22:01:46 +02:00
parent f22e918048
commit af073f87da
6 changed files with 111 additions and 16 deletions

View File

@@ -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<HTMLInputElement>("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");

View File

@@ -204,6 +204,12 @@ function setActiveShape(shape: RenderShape | null): void {
document.querySelectorAll<HTMLButtonElement>("#views-shape-chips [data-shape]").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.shape === shape);
});
// Mirror the active shape on <body> 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;

View File

@@ -104,7 +104,7 @@ export function renderKostenrechner(): string {
<title data-i18n="kosten.title">Prozesskostenrechner &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<body className="has-sidebar page-kostenrechner">
<Sidebar currentPath="/tools/kostenrechner" />
<BottomNav currentPath="/tools/kostenrechner" />

View File

@@ -27,7 +27,7 @@ export function renderProjectsChart(): string {
<title data-i18n="projects.chart.title">Projekt-Chart &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<body className="has-sidebar page-projects-chart">
<Sidebar currentPath="/projects" />
<BottomNav currentPath="/projects" />

View File

@@ -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,

View File

@@ -84,7 +84,7 @@ export function renderVerfahrensablauf(): string {
<title data-i18n="tools.verfahrensablauf.title">Verfahrensablauf &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<body className="has-sidebar page-verfahrensablauf">
<Sidebar currentPath="/tools/verfahrensablauf" />
<BottomNav currentPath="/tools/verfahrensablauf" />