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("") ? withCss.replace("", `${previewScript}`) : `${withCss}${previewScript}`; } @@ -103,6 +104,10 @@ export default function CvBuilderEditor() { const undoStack = useRef([]); const redoStack = useRef([]); const pageMetrics = useMemo(() => getCvPageMetrics(settings?.pageSize), [settings?.pageSize]); + const pageMarginMm = useMemo(() => { + const themeMargin = themes.find((theme) => theme.id === settings?.themeId)?.pageMarginMm; + return settings?.pageMarginMm ?? themeMargin ?? 16; + }, [settings?.pageMarginMm, settings?.themeId, themes]); useEffect(() => { let alive = true; @@ -482,7 +487,7 @@ export default function CvBuilderEditor() { {previewOverflow && The preview reported horizontal overflow. Shorten an unbroken value or retry after the latest render.} {pages >= 3 && This CV is {pages} pages. Content remains readable, but consider hiding less relevant entries for a more focused application.} -