fix(cv): qualify responsive editor and preview workflow

This commit is contained in:
cesnimda
2026-10-09 04:12:06 +02:00
parent ef661983a0
commit 46a0c0c6d2
62 changed files with 32741 additions and 20 deletions
+31 -13
View File
@@ -527,7 +527,11 @@ export default function CvBuilderEditor() {
const canRedo = historyRevision >= 0 && redoStack.current.length > 0;
return (
<Stack spacing={1.5} sx={{ minWidth: 0 }}>
<Stack spacing={1.5} data-content-density="comfortable" sx={{
minWidth: 0,
"--cv-pane-top": "calc(var(--app-shell-header-height, 76px) + 12px)",
"& [tabindex], & button, & input, & textarea, & [role=combobox]": { scrollMarginTop: "var(--cv-pane-top)" },
}}>
{saveState === "conflict" && <Alert severity="warning">
{t("cvEditorConflictMessage")}
<Stack direction="row" gap={1} flexWrap="wrap" sx={{ mt: 1 }}>
@@ -570,7 +574,7 @@ export default function CvBuilderEditor() {
</Paper>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "minmax(360px, 1fr) minmax(0, 1fr)" }, gap: 1.5, alignItems: "start" }}>
<Paper ref={editorRef} tabIndex={-1} variant="outlined" role="region" aria-label={t("cvEditorEditingRegion")} onFocusCapture={() => setMobilePane("edit")} sx={{ display: compactEditor && mobilePane !== "edit" ? "none" : "block", position: { md: "sticky" }, top: 12, minWidth: 0, maxHeight: { md: "calc(100vh - 104px)" }, overflowY: { md: "auto" }, scrollbarWidth: "none", "&::-webkit-scrollbar": { display: "none" } }}>
<Paper ref={editorRef} tabIndex={-1} variant="outlined" role="region" aria-label={t("cvEditorEditingRegion")} onFocusCapture={() => setMobilePane("edit")} sx={{ display: compactEditor && mobilePane !== "edit" ? "none" : "block", position: { md: "sticky" }, top: "var(--cv-pane-top)", minWidth: 0, maxHeight: { md: "calc(100dvh - var(--cv-pane-top) - 12px)" }, overflowY: { md: "auto" }, scrollbarWidth: "thin" }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ px: 1.5, py: 0.5, borderBottom: "1px solid", borderColor: "divider" }}>
<Typography component="h2" variant="subtitle2">{t("cvEditorEditingRegion")}</Typography>
<Stack direction="row">
@@ -585,7 +589,7 @@ export default function CvBuilderEditor() {
<Box role="tabpanel" id="cv-studio-panel-1" aria-labelledby="cv-studio-tab-1" hidden={tab !== 1}>
<ContentTab settings={settings} update={update} outline={outline} onGenerate={generateAi} onAccept={acceptAi} showPreviewEntry={!compactEditor} onFocusPreview={() => {
scrollRef.current?.focus({ preventScroll: true });
scrollRef.current?.scrollIntoView({ block: "nearest", inline: "nearest" });
scrollRef.current?.parentElement?.scrollIntoView?.({ block: "start", inline: "nearest" });
}} />
</Box>
{[0, 2, 3, 4].map((value) => <Box key={value} role="tabpanel" id={`cv-studio-panel-${value}`} aria-labelledby={`cv-studio-tab-${value}`} hidden={tab !== value}>
@@ -599,7 +603,14 @@ export default function CvBuilderEditor() {
</Box>
</Paper>
<Paper variant="outlined" onFocusCapture={() => setMobilePane("preview")} sx={{ display: compactEditor && mobilePane !== "preview" ? "none" : "block", minWidth: 0 }}>
<Paper variant="outlined" onFocusCapture={(event) => {
setMobilePane("preview");
const pane = event.currentTarget;
requestAnimationFrame(() => {
if (pane.contains(document.activeElement)) pane.scrollIntoView?.({ block: "start", inline: "nearest" });
});
}} sx={{ display: compactEditor && mobilePane !== "preview" ? "none" : "flex", flexDirection: "column", minWidth: 0, position: { md: "sticky" }, top: "var(--cv-pane-top)", height: "calc(100dvh - var(--cv-pane-top) - 12px)", scrollMarginTop: "var(--cv-pane-top)" }}>
<Box sx={{ flex: "0 1 auto", minHeight: 0, maxHeight: "50%", overflowY: "auto", scrollbarWidth: "thin" }}>
<Stack direction="row" alignItems="center" gap={1} sx={{ px: 1.5, py: 1, flexWrap: "wrap", borderBottom: "1px solid", borderColor: "divider" }}>
<Box sx={{ flex: "1 1 140px", minWidth: 0 }}>
<Typography component="h2" variant="subtitle2">{t("cvEditorLivePreview")}</Typography>
@@ -612,7 +623,7 @@ export default function CvBuilderEditor() {
setMobilePane("edit");
requestAnimationFrame(() => {
editorRef.current?.focus({ preventScroll: true });
editorRef.current?.scrollIntoView({ block: "nearest", inline: "nearest" });
editorRef.current?.scrollIntoView({ block: "start", inline: "nearest" });
});
}}>{t("cvEditorFocusEditor")}</Button>
</Stack>
@@ -620,11 +631,18 @@ export default function CvBuilderEditor() {
<Typography variant="body2" fontWeight={600}>{t("cvEditorPreviewUnavailable")}</Typography>
<Typography variant="body2">{t(html ? "cvEditorPreviewPrevious" : "cvEditorPreviewEmpty")}</Typography>
</Alert>}
{previewReady && previewOverflow && <Alert severity="warning" sx={{ m: 1 }}>{t("cvEditorOverflowWarning")}</Alert>}
{previewReady && pages >= 3 && <Alert severity="info" sx={{ m: 1 }}>{t("cvEditorLongDocument", { pages })}</Alert>}
{!!html && previewOverflow && <Alert severity="warning" aria-hidden={!previewReady} sx={{ m: 1, visibility: previewReady ? "visible" : "hidden" }}>{t("cvEditorOverflowWarning")}</Alert>}
{!!html && pages >= 3 && <Alert severity="info" aria-hidden={!previewReady} sx={{ m: 1, visibility: previewReady ? "visible" : "hidden" }}>{t("cvEditorLongDocument", { pages })}</Alert>}
<Typography id="cv-preview-help" variant="caption" color="text.secondary" sx={{ display: "block", px: 1.5, py: 0.75 }}>{t("cvEditorPreviewHelp")}</Typography>
</Box>
<iframe ref={iframeRef} title={t("cvEditorPreviewTitle")} srcDoc={previewDocumentHtml(html, pageMetrics.heightPx)} sandbox="allow-same-origin allow-scripts" onLoad={onIframeLoad} aria-hidden tabIndex={-1} style={{ position: "absolute", left: "-10000px", top: 0, width: `${pageMetrics.widthMm}mm`, height: `${pageMetrics.heightMm}mm`, border: "none", visibility: "hidden", pointerEvents: "none" }} />
<Box ref={scrollRef} role="region" aria-label={t("cvEditorLivePreview")} aria-describedby="cv-preview-help" aria-busy={previewBusy} tabIndex={0} sx={{ overflow: "auto", maxHeight: "86vh", px: 1, py: 1.5, bgcolor: "background.default", scrollbarWidth: "thin" }}>
<Box ref={scrollRef} role="region" aria-label={t("cvEditorLivePreview")} aria-describedby="cv-preview-help" aria-busy={previewBusy} tabIndex={0} onKeyDown={(event) => {
// Keep document-boundary keys in this pane, including when already at its edge.
if (event.target === event.currentTarget && event.ctrlKey && (event.key === "Home" || event.key === "End")) {
event.preventDefault();
event.currentTarget.scrollTop = event.key === "Home" ? 0 : event.currentTarget.scrollHeight;
}
}} sx={{ overflow: "auto", flex: "1 1 0", minHeight: 0, maxHeight: "86vh", px: 1, py: 1.5, bgcolor: "background.default", scrollbarWidth: "thin" }}>
<Stack spacing={2} alignItems="center">
{!html && previewBusy && <Skeleton variant="rectangular" width="100%" height={320} />}
{!!html && Array.from({ length: pages }).map((_, index) => (
@@ -1009,8 +1027,8 @@ function ContentTab({ settings, update, outline, showPreviewEntry, onFocusPrevie
};
return (
<Stack spacing={2}>
<Stack direction="row" gap={1} alignItems="center">
<Box component="nav" aria-label={t("cvEditorSectionNavigation")} sx={{ flex: 1, minWidth: 0 }}>
<Stack direction="row" gap={1} alignItems="center" flexWrap="wrap">
<Box component="nav" aria-label={t("cvEditorSectionNavigation")} sx={{ flex: "1 1 14rem", minWidth: 0 }}>
<TextField select fullWidth size="small" label={t("cvEditorGoToSection")}
value={sectionRows.some((row) => row.key === activeSection) ? activeSection : ""}
onChange={(event) => setActiveSection(event.target.value)}>
@@ -1071,7 +1089,7 @@ function ContentTab({ settings, update, outline, showPreviewEntry, onFocusPrevie
</Box>
<Button ref={addContentButton} variant="contained" startIcon={<AddIcon />} onClick={() => setAddContentOpen(true)} sx={{ alignSelf: "center", minWidth: 190 }}>{t("cvEditorAddContent")}</Button>
<Dialog open={addContentOpen} onClose={() => setAddContentOpen(false)} fullWidth maxWidth="md" aria-labelledby="cv-add-content-title">
<Dialog open={addContentOpen} onClose={() => setAddContentOpen(false)} fullWidth maxWidth="md" aria-labelledby="cv-add-content-title" data-content-density="comfortable">
<DialogTitle id="cv-add-content-title" sx={{ fontWeight: 850 }}>{t("cvEditorAddContent")}</DialogTitle>
<DialogContent dividers>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>{t("cvEditorAddContentHelp")}</Typography>
@@ -1159,13 +1177,13 @@ function SectionRow({
minWidth: 0,
}}
>
<Stack direction="row" alignItems="center" gap={0.5} sx={{ px: 0.75, py: 0.5 }}>
<Stack direction="row" alignItems="center" gap={0.5} flexWrap="wrap" sx={{ px: 0.75, py: 0.5 }}>
<DragIndicatorIcon sx={{ fontSize: 18, color: "text.disabled", cursor: "grab" }} aria-hidden />
<Stack>
<IconButton size="small" aria-label={t("cvEditorMoveSectionUp", { section: sectionLabel })} disabled={index === 0} onClick={() => onMove(-1)}><ArrowUpwardIcon sx={{ fontSize: 15 }} /></IconButton>
<IconButton size="small" aria-label={t("cvEditorMoveSectionDown", { section: sectionLabel })} disabled={index === total - 1} onClick={() => onMove(1)}><ArrowDownwardIcon sx={{ fontSize: 15 }} /></IconButton>
</Stack>
<TextField variant="standard" fullWidth value={sectionLabel} sx={{ minWidth: 0 }}
<TextField variant="standard" fullWidth value={sectionLabel} sx={{ flex: "1 1 10rem", minWidth: 0 }}
onChange={(e) => customSection ? onRenameCustom(e.target.value) : onPatch({ title: e.target.value })}
slotProps={{ input: { disableUnderline: true }, htmlInput: { "aria-label": t("cvEditorSectionName", { section: row.key }) } }} />
<IconButton size="small" aria-label={t(row.hidden ? "cvEditorShowSection" : "cvEditorHideSection", { section: sectionLabel })} onClick={() => onPatch({ hidden: !row.hidden })}>