Document Assembler & PDF Export
A block checklist pattern for assembling modular document sections — toggle visibility, reorder by drag or arrow buttons, and export to PDF using CTFramesetter. The assembler drives ResumeExporter which renders each section into an A4 page using Sitka accent colours for headings.
Preview
Document Assembler5 of 7 sections
SU
Professional SummaryEX
Work ExperienceED
EducationSK
Core SkillsPR
Key ProjectsCE
CertificationsCU
Additional InformationClick a row to see section details · toggle the checkbox to include/exclude · use arrows to reorder
Anatomy
The assembler has four distinct layers that compose the final document.
| Layer | Responsibility | Platform | Output |
|---|---|---|---|
| Section Registry | Declares available block types (experience, education, skills, summary, projects, certifications, custom) | Shared model | ResumeSection.swift struct |
| Assembler View | Checklist UI — toggle, reorder, preview summary, trigger export | iOS / macOS / Web | DocumentAssemblerView |
| PDF Renderer | Iterates included sections, lays out content on A4 pages using CTFramesetter | Shared core | single-page or multi-page PDF |
| Share / Save | Delivers the PDF to the user — ShareSheet on iOS, NSSavePanel on macOS | Platform-specific | PDF file in chosen location |
Design notes
→
Every block is self-contained — Each ResumeSection carries its own kind, title, included flag, and sort order. The assembler never mutates the source data — it produces an ordered array of references that the renderer consumes.
→
Heading styles come from Sitka tokens — Use the accent colour token for all H1/H2 headings in the rendered PDF. Body text uses neutral-600 at 11pt on light background. Keep line-height at 1.5 for readability.
→
A4 first, US-Letter fallback — Default page size is A4 (595 × 842 pt). For en-US users, detect locale and swap to US-Letter (612 × 792 pt). The CTFramesetter path handles both — only the page rect changes.
→
Re-order by intent, not drag — iOS: provide up/down chevrons in each row. macOS: support table-row drag reorder via onMove. Both should commit the order change immediately to the array source of truth.
→
Export is a side effect — The PDF generation is intentionally synchronous (CTFramesetter). For long documents (>20 sections) consider offloading to a background Task and showing a progress sheet.
Implementation
DocumentAssembler.tsx
tsx
"use client";
import { useState, useCallback } from "react";
interface ResumeSection {
id: string;
kind: string;
title: string;
included: boolean;
order: number;
}
export function DocumentAssembler({
initialSections,
onExport,
}: {
initialSections: ResumeSection[];
onExport: (sections: ResumeSection[]) => void;
}) {
const [sections, setSections] = useState(initialSections);
const toggle = useCallback((id: string) => {
setSections((prev) =>
prev.map((s) => (s.id === id ? { ...s, included: !s.included } : s))
);
}, []);
const reorder = useCallback((id: string, direction: -1 | 1) => {
setSections((prev) => {
const idx = prev.findIndex((s) => s.id === id);
const swap = idx + direction;
if (swap < 0 || swap >= prev.length) return prev;
const next = [...prev];
[next[idx], next[swap]] = [next[swap], next[idx]];
return next.map((s, i) => ({ ...s, order: i }));
});
}, []);
return (
<div className="space-y-1">
{sections.map((s) => (
<div
key={s.id}
className={[
"flex items-center gap-3 px-3 py-2 rounded-lg",
s.included ? "bg-[rgb(var(--surface-raised))]" : "opacity-40",
].join(" ")}
>
<button
onClick={() => toggle(s.id)}
className="w-5 h-5 rounded flex items-center justify-center
border border-[rgb(var(--border))]"
>
{s.included && (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2 6l3 3 5-5" stroke="rgb(var(--accent))"
strokeWidth="1.5" strokeLinecap="round" />
</svg>
)}
</button>
<span className="text-[13px] flex-1">{s.title}</span>
{s.included && (
<div className="flex gap-1">
<button onClick={() => reorder(s.id, -1)}
className="px-1 text-[rgb(var(--text-tertiary))]">↑</button>
<button onClick={() => reorder(s.id, 1)}
className="px-1 text-[rgb(var(--text-tertiary))]">↓</button>
</div>
)}
</div>
))}
<button className="mt-3 px-4 py-2 rounded-xl bg-[rgb(var(--accent))]
text-white text-[13px] font-semibold"
onClick={() => onExport(sections)}>
Export PDF
</button>
</div>
);
}Privacy & entitlements
| Key / Entitlement | Platform | Notes |
|---|---|---|
| com.apple.security.files.user-selected.read-write | macOS entitlement | Required for NSSavePanel to write PDF to user-chosen location. |
| UIFileSharingEnabled | iOS Info.plist | Not required for ShareLink path; only needed if writing to Documents/ directly. |
Accessibility
- →Each section row must expose an accessibilityLabel combining kind + included state: 'Work Experience — included' or 'Certifications — excluded'.
- →The toggle checkbox must have an accessibilityHint explaining the consequence: 'Toggle to include this section in the exported document'.
- →The export button label must describe the action, not just the document type: 'Export resume as PDF' rather than 'Export'.
- →The PDF output must carry embedded metadata with document title, author, and creation date so screen readers can announce it when opened in Preview or Books.