fix(cv): restore distinct responsive templates

This commit is contained in:
cesnimda
2026-09-01 08:54:03 +02:00
parent 9252e43ad4
commit c8b4c33acd
8 changed files with 161 additions and 23 deletions
+28 -13
View File
@@ -37,7 +37,7 @@ import { useDragReorder } from "../hooks/useDragReorder";
import {
CUSTOM_SECTION_PRESETS, CvCustomSectionSetting, CvItemOverride, CvOutline, CvOutlineSection, CvSectionSetting,
CvTheme, CvVariant, CvVariantSettings, CvVariantVersionInfo, DEFAULT_SECTION_ORDER, SECTION_LABELS,
cvBuilderApi, getCvPageCount, getCvPageMetrics, moveItem, normalizeCvLanguage,
applyCvTheme, cvBuilderApi, getCvPageCount, getCvPageMetrics, getCvPreviewFitZoom, moveItem, normalizeCvLanguage,
} from "../cvBuilder";
import { useDialogActions } from "../dialogs";
import { useI18n } from "../i18n/I18nProvider";
@@ -84,6 +84,7 @@ 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);
@@ -409,10 +410,24 @@ export default function CvBuilderEditor() {
if (target && scrollRef.current) scrollRef.current.scrollTo({ top: Math.max(0, target.offsetTop - 12), behavior: "smooth" });
};
const fitPreview = () => {
const availableWidth = Math.max(1, (scrollRef.current?.clientWidth ?? pageMetrics.widthPx) - 24);
setZoom(Math.min(1, Math.max(MIN_PREVIEW_ZOOM, availableWidth / pageMetrics.widthPx)));
};
const fitPreview = useCallback(() => {
setZoom(getCvPreviewFitZoom(scrollRef.current?.clientWidth ?? pageMetrics.widthPx, pageMetrics.widthPx, 24, MIN_PREVIEW_ZOOM));
}, [pageMetrics.widthPx]);
useEffect(() => {
if (!autoFitPreview) return;
const container = scrollRef.current;
if (!container) return;
const resize = () => fitPreview();
resize();
window.addEventListener("resize", resize);
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(resize);
observer?.observe(container);
return () => {
window.removeEventListener("resize", resize);
observer?.disconnect();
};
}, [autoFitPreview, compactEditor, fitPreview, mobilePane]);
if (loadError) {
return (
@@ -486,11 +501,11 @@ export default function CvBuilderEditor() {
<Button size="small" disabled={page >= pages} onClick={() => goToPage(page + 1)}>{t("cvEditorNext")}</Button>
</Stack>
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
<IconButton size="small" aria-label={t("cvEditorZoomOut")} onClick={() => setZoom((z) => Math.max(MIN_PREVIEW_ZOOM, +(z - 0.1).toFixed(2)))}><ZoomOutIcon fontSize="small" /></IconButton>
<Slider size="small" value={zoom} min={MIN_PREVIEW_ZOOM} max={1} step={0.02} onChange={(_, v) => setZoom(v as number)} sx={{ width: 90 }} aria-label={t("cvEditorZoom")} />
<IconButton size="small" aria-label={t("cvEditorZoomIn")} onClick={() => setZoom((z) => Math.min(1, +(z + 0.1).toFixed(2)))}><ZoomInIcon fontSize="small" /></IconButton>
<IconButton size="small" aria-label={t("cvEditorZoomOut")} onClick={() => { setAutoFitPreview(false); setZoom((z) => Math.max(MIN_PREVIEW_ZOOM, +(z - 0.1).toFixed(2))); }}><ZoomOutIcon fontSize="small" /></IconButton>
<Slider size="small" value={zoom} min={MIN_PREVIEW_ZOOM} max={1} step={0.02} onChange={(_, v) => { setAutoFitPreview(false); setZoom(v as number); }} sx={{ width: 90 }} aria-label={t("cvEditorZoom")} />
<IconButton size="small" aria-label={t("cvEditorZoomIn")} onClick={() => { setAutoFitPreview(false); setZoom((z) => Math.min(1, +(z + 0.1).toFixed(2))); }}><ZoomInIcon fontSize="small" /></IconButton>
<Typography variant="caption" sx={{ minWidth: 34, textAlign: "right" }}>{Math.round(zoom * 100)}%</Typography>
<Button size="small" onClick={fitPreview}>{t("cvEditorFit")}</Button>
<Button size="small" variant={autoFitPreview ? "outlined" : "text"} onClick={() => { setAutoFitPreview(true); fitPreview(); }}>{t("cvEditorFit")}</Button>
</Stack>
{previewOverflow && <Alert severity="warning" sx={{ mb: 1 }}>{t("cvEditorOverflowWarning")}</Alert>}
{pages >= 3 && <Alert severity="info" sx={{ mb: 1 }}>{t("cvEditorLongDocument", { pages })}</Alert>}
@@ -1137,16 +1152,16 @@ function CustomizeTab({ mode, settings, update, themes }: {
{mode === "template" && <Box>
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>{t("cvEditorTheme")}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mb: 1.25 }}>{t("cvEditorThemeHelp")}</Typography>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1 }}>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1.25 }}>
{themes.map((theme) => {
const active = theme.id === settings.themeId;
const locked = theme.available === false;
return (
<Paper key={theme.id} variant="outlined" role="button" tabIndex={locked ? -1 : 0} aria-disabled={locked}
onClick={() => { if (!locked) update({ themeId: theme.id }); }}
onKeyDown={(e) => { if (!locked && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); update({ themeId: theme.id }); } }}
onClick={() => { if (!locked) update(applyCvTheme(settings, theme.id)); }}
onKeyDown={(e) => { if (!locked && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); update(applyCvTheme(settings, theme.id)); } }}
sx={{ p: 1, cursor: locked ? "not-allowed" : "pointer", opacity: locked ? 0.6 : 1, outline: "none", borderColor: active ? "primary.main" : undefined, borderWidth: active ? 2 : 1, "&:focus-visible": { boxShadow: 3 } }}>
<Box sx={{ mb: 1 }}><CvTemplateThumbnail theme={theme} height={110} /></Box>
<Box sx={{ mb: 1 }}><CvTemplateThumbnail theme={theme} height={170} /></Box>
<Typography variant="body2" sx={{ fontWeight: 700 }}>{theme.name}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: "block" }}>{theme.category}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 0.25, lineHeight: 1.3 }}>{theme.description}</Typography>