fix(cv): qualify responsive editor and preview workflow
This commit is contained in:
@@ -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 })}>
|
||||
|
||||
Reference in New Issue
Block a user