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 Summary
EX
Work Experience
ED
Education
SK
Core Skills
PR
Key Projects
CE
Certifications
CU
Additional Information

Click 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.

LayerResponsibilityPlatformOutput
Section RegistryDeclares available block types (experience, education, skills, summary, projects, certifications, custom)Shared modelResumeSection.swift struct
Assembler ViewChecklist UI — toggle, reorder, preview summary, trigger exportiOS / macOS / WebDocumentAssemblerView
PDF RendererIterates included sections, lays out content on A4 pages using CTFramesetterShared coresingle-page or multi-page PDF
Share / SaveDelivers the PDF to the user — ShareSheet on iOS, NSSavePanel on macOSPlatform-specificPDF file in chosen location

Design notes

Every block is self-containedEach 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 tokensUse 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 fallbackDefault 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 dragiOS: 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 effectThe 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 / EntitlementPlatformNotes
com.apple.security.files.user-selected.read-writemacOS entitlementRequired for NSSavePanel to write PDF to user-chosen location.
UIFileSharingEnablediOS Info.plistNot 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.