From e912b72110e54b0341f690e02e5c52bfcf158483 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Tue, 1 Sep 2026 16:37:51 +0200 Subject: [PATCH] fix(cv): restore distinct frameless previews --- .../src/cv-builder-deep-link.test.tsx | 4 +- job-tracker-ui/src/cvBuilder.test.ts | 12 +++-- job-tracker-ui/src/cvBuilder.ts | 20 ++++++-- job-tracker-ui/src/i18n/translations.ts | 4 ++ job-tracker-ui/src/views/CvBuilderEditor.tsx | 51 +++++-------------- 5 files changed, 46 insertions(+), 45 deletions(-) diff --git a/job-tracker-ui/src/cv-builder-deep-link.test.tsx b/job-tracker-ui/src/cv-builder-deep-link.test.tsx index aa6b954..fb80d86 100644 --- a/job-tracker-ui/src/cv-builder-deep-link.test.tsx +++ b/job-tracker-ui/src/cv-builder-deep-link.test.tsx @@ -184,6 +184,7 @@ test('choosing a template restores its own structure instead of inheriting the p sidebarWidthMm: 72, sidebarSections: ['skills'], accentColor: '#123456', + pageMarginMm: 9, }, }; mockedApi.get.mockImplementation((url: string) => { @@ -212,7 +213,8 @@ test('choosing a template restores its own structure instead of inheriting the p headingStyle: null, sidebarWidthMm: null, sidebarSections: null, - accentColor: '#123456', + accentColor: null, + pageMarginMm: null, }), }))); }); diff --git a/job-tracker-ui/src/cvBuilder.test.ts b/job-tracker-ui/src/cvBuilder.test.ts index 908e377..1e71afa 100644 --- a/job-tracker-ui/src/cvBuilder.test.ts +++ b/job-tracker-ui/src/cvBuilder.test.ts @@ -63,12 +63,12 @@ describe("CV language compatibility", () => { test("fits the physical page to the available preview width", () => { expect(getCvPreviewFitZoom(500, 800)).toBeCloseTo(0.595); expect(getCvPreviewFitZoom(1200, 800)).toBe(1); - expect(getCvPreviewFitZoom(120, 800)).toBe(0.32); + expect(getCvPreviewFitZoom(120, 800, 24, 0.2)).toBe(0.2); }); }); describe("CV template selection", () => { - test("restores the selected template structure without discarding user content or design choices", () => { + test("restores the selected template visual defaults without discarding user content", () => { const settings = { themeId: "minimal", layout: "single" as const, @@ -77,6 +77,9 @@ describe("CV template selection", () => { sidebarWidthMm: 70, sidebarSections: ["skills"], accentColor: "#123456", + headingFont: "Georgia, 'Times New Roman', serif", + pageMarginMm: 9, + skillsStyle: "text" as const, showPhoto: false, showPageNumbers: false, showIcons: true, @@ -92,7 +95,10 @@ describe("CV template selection", () => { headingStyle: null, sidebarWidthMm: null, sidebarSections: null, - accentColor: "#123456", + accentColor: null, + headingFont: null, + pageMarginMm: null, + skillsStyle: null, sections: [{ key: "experience", title: "Career" }], }); }); diff --git a/job-tracker-ui/src/cvBuilder.ts b/job-tracker-ui/src/cvBuilder.ts index 2209ac3..f8d22da 100644 --- a/job-tracker-ui/src/cvBuilder.ts +++ b/job-tracker-ui/src/cvBuilder.ts @@ -182,16 +182,30 @@ export function getCvPreviewFitZoom(containerWidthPx: number, pageWidthPx: numbe return Math.min(1, Math.max(minZoom, usableWidth / pageWidthPx)); } -// Selecting a template is a structural choice. Clear only the presentation overrides that can -// flatten every template into the previous template's layout; keep content, colour, typography, -// spacing and document settings intact. +// Selecting a template restores that template's complete visual identity. CV content and document +// metadata remain untouched; users can customise the newly selected template afterwards. export function applyCvTheme(settings: CvVariantSettings, themeId: string): CvVariantSettings { return { ...settings, themeId, + accentColor: null, + headingFont: null, + bodyFont: null, + density: null, + textColor: null, + mutedColor: null, + headingColor: null, + backgroundColor: null, + baseFontSizePt: null, + headingSizePt: null, + lineHeight: null, + pageMarginMm: null, + sectionGapMm: null, + entryGapMm: null, layout: null, headerStyle: null, headingStyle: null, + skillsStyle: null, sidebarWidthMm: null, sidebarSections: null, }; diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index dd6f27b..4a8329f 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -506,6 +506,8 @@ export const translations = { cvEditorUpdating: "Updating…", cvEditorPublic: "Public", cvEditorPrivate: "Private", + cvEditorShare: "Share", + cvEditorStopSharing: "Stop sharing", cvEditorCopyLink: "Copy link", cvEditorExporting: "Exporting…", cvEditorDownloadPdf: "Download PDF", @@ -2926,6 +2928,8 @@ export const translations = { cvEditorUpdating: "Oppdaterer…", cvEditorPublic: "Offentlig", cvEditorPrivate: "Privat", + cvEditorShare: "Del", + cvEditorStopSharing: "Stopp deling", cvEditorCopyLink: "Kopier lenke", cvEditorExporting: "Eksporterer…", cvEditorDownloadPdf: "Last ned PDF", diff --git a/job-tracker-ui/src/views/CvBuilderEditor.tsx b/job-tracker-ui/src/views/CvBuilderEditor.tsx index 8e163b1..2bd9d11 100644 --- a/job-tracker-ui/src/views/CvBuilderEditor.tsx +++ b/job-tracker-ui/src/views/CvBuilderEditor.tsx @@ -3,7 +3,7 @@ import { Link as RouterLink, useBlocker, useNavigate, useParams } from "react-ro import { Alert, Box, Button, Chip, Collapse, Dialog, DialogActions, DialogContent, DialogTitle, Divider, IconButton, - MenuItem, Paper, Skeleton, Slider, Stack, Tab, Tabs, TextField, Tooltip, Typography, + MenuItem, Paper, Skeleton, Stack, Tab, Tabs, TextField, Tooltip, Typography, } from "@mui/material"; import useMediaQuery from "@mui/material/useMediaQuery"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; @@ -18,8 +18,6 @@ import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import ZoomInIcon from "@mui/icons-material/ZoomIn"; -import ZoomOutIcon from "@mui/icons-material/ZoomOut"; import UndoIcon from "@mui/icons-material/Undo"; import RedoIcon from "@mui/icons-material/Redo"; import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh"; @@ -42,14 +40,14 @@ import { import { useDialogActions } from "../dialogs"; import { useI18n } from "../i18n/I18nProvider"; -const MIN_PREVIEW_ZOOM = 0.32; +const MIN_PREVIEW_ZOOM = 0.2; type SaveState = "idle" | "unsaved" | "saving" | "saved" | "error"; 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 = ``; @@ -75,13 +73,11 @@ export default function CvBuilderEditor() { const [tab, setTab] = useState(0); const [html, setHtml] = useState(""); const [zoom, setZoom] = useState(0.62); - const [autoFitPreview, setAutoFitPreview] = useState(true); const [previewing, setPreviewing] = useState(false); const [previewError, setPreviewError] = useState(false); const [previewRevision, setPreviewRevision] = useState(0); const [previewOverflow, setPreviewOverflow] = useState(false); const [pages, setPages] = useState(1); - const [page, setPage] = useState(1); const [saveState, setSaveState] = useState("idle"); const [exporting, setExporting] = useState(false); const [publishing, setPublishing] = useState(false); @@ -381,7 +377,6 @@ export default function CvBuilderEditor() { ); const pageCount = getCvPageCount(h, pageMetrics.heightPx); setPages(pageCount); - setPage((current) => Math.min(current, pageCount)); const viewportWidth = doc?.documentElement?.clientWidth ?? pageMetrics.widthPx; const contentWidth = Math.max(doc?.body?.scrollWidth ?? 0, doc?.documentElement?.scrollWidth ?? 0); setPreviewOverflow(contentWidth > viewportWidth + 2); @@ -394,19 +389,11 @@ export default function CvBuilderEditor() { else measure(); }; - const goToPage = (p: number) => { - const clamped = Math.min(Math.max(1, p), pages); - setPage(clamped); - const target = scrollRef.current?.querySelector(`[data-cv-page="${clamped}"]`); - if (target && scrollRef.current) scrollRef.current.scrollTo({ top: Math.max(0, target.offsetTop - 12), behavior: "smooth" }); - }; - const fitPreview = useCallback(() => { - setZoom(getCvPreviewFitZoom(scrollRef.current?.clientWidth ?? pageMetrics.widthPx, pageMetrics.widthPx, 24, MIN_PREVIEW_ZOOM)); + setZoom(getCvPreviewFitZoom(scrollRef.current?.clientWidth ?? pageMetrics.widthPx, pageMetrics.widthPx, 16, MIN_PREVIEW_ZOOM)); }, [pageMetrics.widthPx]); useEffect(() => { - if (!autoFitPreview) return; const container = scrollRef.current; if (!container) return; const resize = () => fitPreview(); @@ -418,7 +405,7 @@ export default function CvBuilderEditor() { window.removeEventListener("resize", resize); observer?.disconnect(); }; - }, [autoFitPreview, compactEditor, fitPreview, mobilePane]); + }, [compactEditor, fitPreview, mobilePane]); if (loadError) { return ( @@ -453,7 +440,7 @@ export default function CvBuilderEditor() { {isPublic && } @@ -478,35 +465,23 @@ export default function CvBuilderEditor() { {tab === 4 && } - - + + {t("cvEditorLivePreview")} {previewing && } {previewError && } {previewError && } - {pages >= 3 && } - - - {t("cvEditorPageOf", { page, pages })} - - - - { setAutoFitPreview(false); setZoom((z) => Math.max(MIN_PREVIEW_ZOOM, +(z - 0.1).toFixed(2))); }}> - { setAutoFitPreview(false); setZoom(v as number); }} sx={{ width: 90 }} aria-label={t("cvEditorZoom")} /> - { setAutoFitPreview(false); setZoom((z) => Math.min(1, +(z + 0.1).toFixed(2))); }}> - {Math.round(zoom * 100)}% - + {t("cvEditorPageCount", { count: pages })} {previewOverflow && {t("cvEditorOverflowWarning")}} {pages >= 3 && {t("cvEditorLongDocument", { pages })}}