feat(export): separate resume/cover-letter downloads, redesign dialog, add Markdown export (#3217)

* feat(export): separate resume/cover-letter downloads, redesign dialog, add Markdown

Let people export the resume and cover letter as distinct documents, and add a
Markdown format alongside PDF / DOCX / JSON (handy for AI agents).

- Server/API: scope PDF generation and download URLs to a resume/cover-letter target.
- Export domain: getResumeExportData + resumeHasCoverLetter in @reactive-resume/resume.
- Redesign the download dialog: one global "What to export" scope toggle (Tabs) plus
  flattened per-format rows, reusing existing UI components and design language.
- Add Markdown export (@reactive-resume/resume/markdown) with a small tiptap-HTML converter.
- Fix blank section headings in DOCX and Markdown by injecting the locale-aware
  section-title resolver (titles are stored empty and resolved at render time).
- Locale catalogs updated for the new strings.

* test(e2e): open the download dialog before exporting JSON

The JSON export moved into the redesigned download dialog, so the spec now opens
the dialog from the Export sidebar section before clicking "Download JSON".
This commit is contained in:
Amruth Pillai
2026-07-05 14:40:49 +02:00
committed by GitHub
parent 6e7fc68068
commit 20c803e934
76 changed files with 2919 additions and 1773 deletions
@@ -6,13 +6,10 @@ import {
CircleNotchIcon,
CopySimpleIcon,
DownloadSimpleIcon,
FileDocIcon,
FileJsIcon,
HouseSimpleIcon,
LockSimpleIcon,
LockSimpleOpenIcon,
PencilSimpleLineIcon,
PrinterIcon,
SidebarSimpleIcon,
TrashSimpleIcon,
WarningCircleIcon,
@@ -31,7 +28,7 @@ import {
} from "@reactive-resume/ui/components/dropdown-menu";
import { useDialogStore } from "@/dialogs/store";
import { useCurrentResume, usePatchResume, useResumeStore } from "@/features/resume/builder/draft";
import { useResumeExport } from "@/features/resume/export/use-resume-export";
import { ResumeDownloadDialog } from "@/features/resume/export/download-dialog";
import { useConfirm } from "@/hooks/use-confirm";
import { getResumeErrorMessage } from "@/libs/error-message";
import { orpc } from "@/libs/orpc/client";
@@ -103,63 +100,31 @@ export function BuilderHeader() {
function ResumeDownloadButton() {
const resume = useCurrentResume();
const { onDownloadPDF, onDownloadDOCX, onDownloadJSON, onPrint, isExporting } = useResumeExport(resume);
return (
<div className="flex items-center">
<Button
size="sm"
aria-label={t({
comment: "Primary action in the builder header to download the resume as a PDF",
message: "Download PDF",
})}
className="rounded-e-none px-2 sm:px-2.5"
disabled={isExporting}
onClick={onDownloadPDF}
>
{isExporting ? (
<CircleNotchIcon className="animate-spin sm:me-1.5" />
) : (
<DownloadSimpleIcon className="sm:me-1.5" />
)}
<span className="hidden sm:inline">
<Trans comment="Primary action in the builder header to download the resume as a PDF">Download PDF</Trans>
</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
size="sm"
disabled={isExporting}
aria-label={t`More download options`}
className="rounded-s-none border-primary-foreground/20 border-s px-1.5"
>
<CaretDownIcon />
</Button>
}
/>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={onDownloadDOCX}>
<FileDocIcon className="me-2" />
<Trans>Download DOCX</Trans>
</DropdownMenuItem>
<DropdownMenuItem onClick={onDownloadJSON}>
<FileJsIcon className="me-2" />
<Trans>Download JSON</Trans>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onPrint}>
<PrinterIcon className="me-2" />
<Trans>Print</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<ResumeDownloadDialog
resume={resume}
trigger={(disabled) => (
<Button
size="sm"
aria-label={t({
comment: "Primary action in the builder header to open resume download options",
message: "Download options",
})}
disabled={disabled}
className="px-2 sm:px-2.5"
>
{disabled ? (
<CircleNotchIcon className="animate-spin sm:me-1.5" />
) : (
<DownloadSimpleIcon className="sm:me-1.5" />
)}
<span className="hidden sm:inline">
<Trans comment="Primary action in the builder header to open resume download options">Download</Trans>
</span>
</Button>
)}
/>
);
}