From 4da673ff001257bc9d429fa16f68d1ae920725ab Mon Sep 17 00:00:00 2001
From: cesnimda
Date: Fri, 28 Aug 2026 12:20:44 +0200
Subject: [PATCH] fix(cv): repeat printable page margins
---
JobTrackerApi.Tests/CvBuilderTests.cs | 3 +++
JobTrackerApi/Controllers/CvVariantController.cs | 1 +
JobTrackerApi/Services/ThemedCvRenderer.cs | 15 ++++++++++++---
job-tracker-ui/src/cvBuilder.ts | 1 +
job-tracker-ui/src/views/CvBuilderEditor.tsx | 15 ++++++++++-----
5 files changed, 27 insertions(+), 8 deletions(-)
diff --git a/JobTrackerApi.Tests/CvBuilderTests.cs b/JobTrackerApi.Tests/CvBuilderTests.cs
index 5ff8dee..61b3abc 100644
--- a/JobTrackerApi.Tests/CvBuilderTests.cs
+++ b/JobTrackerApi.Tests/CvBuilderTests.cs
@@ -291,6 +291,9 @@ public sealed class CvBuilderTests
Assert.Contains("line-height:1.55", html);
Assert.Contains("font-size:13pt", html);
Assert.Contains("class=\"skills-text\">C# · SQL
", html);
+ Assert.Contains("--cv-page-margin:20mm", html);
+ Assert.Contains("@page{size:210mm 297mm;margin:20mm 0;}", html);
+ Assert.Contains(".page{width:auto;min-height:0;margin:0;}", html);
}
[Fact]
diff --git a/JobTrackerApi/Controllers/CvVariantController.cs b/JobTrackerApi/Controllers/CvVariantController.cs
index e543f5c..c742ad4 100644
--- a/JobTrackerApi/Controllers/CvVariantController.cs
+++ b/JobTrackerApi/Controllers/CvVariantController.cs
@@ -60,6 +60,7 @@ public sealed class CvVariantController : ControllerBase
available = proThemes || !t.Premium,
swatches = new[] { t.Accent, t.SidebarBg, t.Paper },
supportedSettings = t.SupportedSettings,
+ pageMarginMm = t.PageMarginMm,
});
return Ok(themes);
}
diff --git a/JobTrackerApi/Services/ThemedCvRenderer.cs b/JobTrackerApi/Services/ThemedCvRenderer.cs
index 4ca6bc2..7d5a627 100644
--- a/JobTrackerApi/Services/ThemedCvRenderer.cs
+++ b/JobTrackerApi/Services/ThemedCvRenderer.cs
@@ -256,7 +256,7 @@ public sealed class ThemedCvRenderer : IThemedCvRenderer
return $@"
*{{box-sizing:border-box;}}
-:root{{--cv-accent-color:{accent};--cv-accent-ink:{headerInk};--cv-accent-soft:color-mix(in srgb,{accent} 28%,transparent);--cv-ink:{ink};--cv-muted:{muted};--cv-paper:{paper};}}
+:root{{--cv-accent-color:{accent};--cv-accent-ink:{headerInk};--cv-accent-soft:color-mix(in srgb,{accent} 28%,transparent);--cv-ink:{ink};--cv-muted:{muted};--cv-paper:{paper};--cv-page-margin:{margin}mm;}}
html,body{{min-width:0;}}
body{{margin:0;background:#e9edf2;color:var(--cv-ink);font-family:{bodyFont};font-size:{F(bodySize)}pt;line-height:{F(lineHeight)};-webkit-print-color-adjust:exact;print-color-adjust:exact;}}
.page{{width:{page.w};min-height:{page.h};margin:0 auto;background:var(--cv-paper);overflow:visible;overflow-wrap:anywhere;word-break:normal;}}
@@ -315,8 +315,17 @@ h1,h2{{font-family:{headingFont};}}
.tag,.contact-item{{break-inside:avoid;}}
.bullets li{{break-inside:avoid-page;page-break-inside:avoid;orphans:2;widows:2;overflow-wrap:anywhere;}}
.bullets li.item-flow{{break-inside:auto;page-break-inside:auto;}}
-@media print{{body{{background:transparent;}}}}
-@page{{size:{page.w} {page.h};margin:0;}}
+/* Physical-page margins belong to paged media rather than an individual template box. This makes
+ the same top/bottom safe area repeat on every page while themes continue to own horizontal
+ layout and density. Screen preview mirrors these bounds in CvBuilderEditor. */
+@media print{{
+body{{background:transparent;}}
+.page{{width:auto;min-height:0;margin:0;}}
+.cols{{min-height:0;}}
+.header{{padding-top:0;}}
+.main,.sidebar{{padding-top:0;padding-bottom:0;}}
+}}
+@page{{size:{page.w} {page.h};margin:{margin}mm 0;}}
";
}
diff --git a/job-tracker-ui/src/cvBuilder.ts b/job-tracker-ui/src/cvBuilder.ts
index 7bf8242..3b09859 100644
--- a/job-tracker-ui/src/cvBuilder.ts
+++ b/job-tracker-ui/src/cvBuilder.ts
@@ -62,6 +62,7 @@ export type CvTheme = {
available: boolean;
swatches: string[];
supportedSettings?: string[];
+ pageMarginMm?: number;
};
// Master profile resolved to sections+entries (GET /api/cv/outline) — what the Content tab edits.
diff --git a/job-tracker-ui/src/views/CvBuilderEditor.tsx b/job-tracker-ui/src/views/CvBuilderEditor.tsx
index 4fb34ac..d3558f7 100644
--- a/job-tracker-ui/src/views/CvBuilderEditor.tsx
+++ b/job-tracker-ui/src/views/CvBuilderEditor.tsx
@@ -48,13 +48,14 @@ const FONT_LABELS = ["Segoe UI", "Arial", "Georgia (serif)", "Helvetica Neue", "
const MIN_PREVIEW_ZOOM = 0.32;
type SaveState = "idle" | "unsaved" | "saving" | "saved" | "error";
-function previewDocumentHtml(html: string, pageHeightPx: number, pageIndex = 0): string {
+function previewDocumentHtml(html: string, pageHeightPx: number, pageMarginMm: number, pageIndex = 0): string {
const height = Math.max(1, Math.round(pageHeightPx * 100) / 100);
+ const margin = Math.max(0, Math.round(pageMarginMm * (96 / 25.4) * 100) / 100);
const offset = Math.max(0, pageIndex) * height;
- const previewCss = ``;
+ const previewCss = ``;
// Screen media does not apply Chromium's paged-media fragmentation. Reproduce the important
// keep-together decisions before each preview frame crops the selected physical page.
- const previewScript = ``;
+ const previewScript = ``;
const withCss = html.includes("") ? html.replace("", `${previewCss}`) : `${previewCss}${html}`;
return withCss.includes("