npm package: @smart-resume/editor
Description: React resume editor and cover letter editor with live preview, templates, ATS scoring, and PDF export
Latest version: 1.4.0

# @smart-resume/editor

A self-contained React component library that provides full resume and cover letter editing with live preview. Drop it into any React app and pass a [JSONResume](https://jsonresume.org/schema/)-compatible object — the component handles editing, live preview, template switching, country profiles, zoom, and an optional AI health panel.

---

## Installation

```bash
npm install @smart-resume/editor
```

Import the stylesheet once in your app entry point:

```ts
import '@smart-resume/editor/dist/styles.css';
```

---

## Quick start

```tsx
import { SmartResumeEditor } from '@smart-resume/editor';
import '@smart-resume/editor/dist/styles.css';

export default function App() {
  return (
    <div style={{ height: '100vh' }}>
      <SmartResumeEditor
        resume={myResume}
        onSave={(updated) => saveToDatabase(updated)}
      />
    </div>
  );
}
```

The component fills its container — give the parent a fixed height.

---

## Exports

| Export | Description |
|--------|-------------|
| `SmartResumeEditor` | Resume editor + preview component |
| `ResumePrintPage` | Bare route component for resume PDF generation (mount at `/print`) |
| `SmartCoverLetterEditor` | Cover letter editor + preview component |
| `CoverLetterPrintPage` | Bare route component for cover letter PDF generation (mount at `/print/cover-letter`) |
| `ResumeComparison` | Inline collapsible diff panel for comparing resume versions |
| `ResumeComparisonEditor` | Full-screen side-by-side editor with live diffs |
| `FullPageComparison` | Full-page read-only comparison overlay |
| `InlineComparison` | Lightweight inline read-only comparison |
| `useResumeComparison` | Core diffing engine for two `JsonResume` objects |
| `useDiffHighlightKeys` | Generates highlight keys for changed fields |
| All types | `JsonResume`, `CoverLetter`, `ResumeTemplate`, `CoverLetterTemplate`, `CountryProfile`, `HealthCheck`, `AISuggestion`, `SreConfig`, `ResumeVersion`, `EditableResumeVersion`, `ComparisonResult`, etc. |

---

## Props

### Required

| Prop | Type | Description |
|------|------|-------------|
| `resume` | `JsonResume` | Initial resume data in JSONResume format |
| `onSave` | `(resume: JsonResume) => void \| Promise<void>` | Called on every edit (auto-save). Debounce on your side if needed |

### Optional

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `onManualSave` | `(resume, editorState) => void \| Promise<void>` | — | If provided, a Save button appears in the header. `editorState` includes template, country, sectionOrder |
| `template` | `ResumeTemplate` | `'classic'` | Controlled template |
| `onTemplateChange` | `(t: ResumeTemplate) => void` | — | Called when user switches template |
| `country` | `CountryProfile` | `'us'` | Controlled country profile |
| `onCountryChange` | `(c: CountryProfile) => void` | — | Called when user switches country |
| `initialSectionOrder` | `string[]` | — | Custom section order override (e.g. `['work', 'education', 'skills']`) |
| `onRewriteBullet` | `(bullet, context) => Promise<string>` | — | AI bullet rewrite. Return the rewritten string |
| `healthChecks` | `HealthCheck[]` | — | Check items to display in the health panel |
| `healthSuggestions` | `AISuggestion[]` | — | AI suggestions to display in the health panel |
| `atsScore` | `number` | — | ATS score (0–100) shown in the health panel |
| `onRecalculateHealth` | `() => void \| Promise<void>` | — | Called when user clicks Re-calculate |
| `documentName` | `string` | — | Editable document name shown in the toolbar. Also used as PDF filename fallback (spaces replaced with underscores) |
| `onDocumentNameChange` | `(name: string) => void` | — | Called when the user renames the document in the toolbar. Required for the name input to appear |
| `pdfServerUrl` | `string` | — | URL of the PDF server. If omitted, the Export PDF button is hidden (unless `onExportPDF` is provided) |
| `pdfHeaders` | `Record<string, string>` | — | Extra headers for PDF generation (e.g. internal secret for local dev) |
| `pdfTitle` | `string` | `resume.basics.name` | Title for exported PDF metadata |
| `pdfFilename` | `string` | `resume.basics.name` | Filename for downloaded PDF (without `.pdf`). Takes precedence over `documentName` |
| `onExportPDF` | `(payload) => void \| Promise<void>` | — | Override entire PDF export flow. If provided, `pdfServerUrl`/`pdfHeaders` are ignored and the Export button is shown |
| `onClose` | `() => void` | — | If provided, an x button appears in the top bar. Useful for modal/drawer usage |
| `config` | `SreConfig` | — | Grouped config object (see below) |
| `className` | `string` | `''` | Extra CSS class on the root element |

---

## Config

Pass a `config` object to control UI behaviour and theming:

```tsx
<SmartResumeEditor
  resume={resume}
  onSave={onSave}
  config={{
    ui: {
      title: 'My Resume Editor',
      autoSave: true,
      showCountryToggle: true,
      showTemplateToggle: true,
      showHealthPanel: true,
      showRecalculate: true,
      healthPanelWidth: 300,
    },
    templates: {
      available: ['classic', 'modern', 'minimal'],
    },
    theme: {
      primary: '270 83% 53%',
    },
  }}
/>
```

### `config.ui`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `title` | `string` | `'Resume Editor'` | Top bar title |
| `autoSave` | `boolean` | `true` | Auto-call `onSave` on every edit |
| `showCountryToggle` | `boolean` | `true` | Show/hide country selector |
| `showTemplateToggle` | `boolean` | `true` | Show/hide template selector |
| `showHealthPanel` | `boolean` | `true` | Show/hide the Resume Health panel |
| `showRecalculate` | `boolean` | `true` | Show/hide the Re-calculate button in the health panel |
| `healthPanelWidth` | `number` | `280` | Health panel width in px |
| `showDocumentName` | `boolean` | `false` | Show an editable document name input in the toolbar (requires `onDocumentNameChange` prop) |

### `config.templates`

| Key | Type | Description |
|-----|------|-------------|
| `available` | `ResumeTemplate[]` | Restrict which templates appear in the picker |

### `config.theme`

Override CSS design tokens. All values are HSL channel strings (no `hsl()` wrapper):

```ts
theme: {
  primary: '221 83% 53%',
  background: '0 0% 100%',
}
```

---

## Document naming

Both editors support an inline-editable document name in the toolbar. When enabled, the toolbar shows the title followed by a `/` separator and the document name (e.g. `Studio / My Resume`). Users can click the name to rename it — press Enter to confirm or Escape to cancel.

The document name also serves as a fallback for the PDF download filename (spaces are replaced with underscores). If `pdfFilename` is explicitly set, it takes precedence.

### Resume editor

```tsx
const [docName, setDocName] = useState('Senior_Engineer_Resume');

<SmartResumeEditor
  resume={resume}
  onSave={onSave}
  documentName={docName}
  onDocumentNameChange={setDocName}
  config={{
    ui: { title: 'Studio', showDocumentName: true },
  }}
/>
```

### Cover letter editor

```tsx
const [docName, setDocName] = useState('Stripe_Cover_Letter');

<SmartCoverLetterEditor
  coverLetter={coverLetter}
  onSave={onSave}
  documentName={docName}
  onDocumentNameChange={setDocName}
  config={{
    ui: { title: 'Cover Letter Studio', showDocumentName: true },
  }}
/>
```

Both `documentName` and `onDocumentNameChange` must be provided, along with `config.ui.showDocumentName: true`, for the input to appear.

---

## Templates

| Value | Description |
|-------|-------------|
| `classic` | Clean, traditional layout |
| `modern` | Blue accent with bold section headers |
| `minimal` | Light, whitespace-forward |
| `executive` | Formal with link section in header |

Templates are defined in `src/templates/index.tsx` as a registry — each template is a `TemplateConfig` object with render functions for `sectionHeader`, `header`, `workItem`, and `educationItem`. Adding a new template means adding one object to the registry with no changes to `ResumePreview`.

### Adding a custom template (fork/extend)

```ts
// src/templates/index.tsx
const myTemplate: TemplateConfig = {
  sectionHeader: (label) => (
    <h2 style={{ borderLeft: '3px solid purple', paddingLeft: 8 }}>{label}</h2>
  ),
  header: (basics, config) => (
    <header>
      <h1>{basics.name}</h1>
      <ContactLine basics={basics} config={config} />
    </header>
  ),
  workItem: classic.workItem,       // reuse existing
  educationItem: classic.educationItem,
};

export const TEMPLATES: Record<ResumeTemplate, TemplateConfig> = {
  classic, modern, minimal, executive,
  myTemplate, // add here — also extend ResumeTemplate type
};
```

---

## Country profiles

| Value | Description |
|-------|-------------|
| `us` | US — standard sections |
| `uk` | UK — standard sections |
| `de` | Germany — includes birth date field |
| `fr` | France — includes birth date field |
| `jp` | Japan |

---

## Preview zoom

The preview panel has built-in zoom controls:

- Auto-fits to the available width on load
- `−` / `+` buttons step by 10%
- Click the percentage label to type an exact value (30–150%), press Enter or blur to apply
- Range: 30% – 150%

Zoom is purely visual — the resume always lays out at exactly 816px (8.5in at 96dpi) before scale is applied, so page breaks are identical at every zoom level and match the exported PDF.

---

## AI integration

### Bullet rewrite

```tsx
<SmartResumeEditor
  resume={resume}
  onSave={onSave}
  onRewriteBullet={async (bullet, { jobTitle, company, allBullets }) => {
    const res = await fetch('/api/rewrite', {
      method: 'POST',
      body: JSON.stringify({ bullet, jobTitle, company, allBullets }),
    });
    const { result } = await res.json();
    return result;
  }}
/>
```

### Health panel

The component never computes health internally — you own that logic and pass the results in:

```tsx
<SmartResumeEditor
  resume={resume}
  onSave={onSave}
  atsScore={78}
  healthChecks={[
    { id: '1', type: 'success', message: 'Strong action verbs in 8/11 bullet points' },
    { id: '2', type: 'warning', message: 'Summary is under 50 words' },
  ]}
  healthSuggestions={[
    {
      id: 's1',
      section: 'work',
      field: 'highlights',
      type: 'impact',
      original: 'Worked on data pipelines',
      suggested: 'Built ETL pipelines processing 2M+ records daily, reducing latency by 40%',
    },
  ]}
  onRecalculateHealth={async () => {
    const result = await analyzeResume(resume);
    setHealthData(result);
  }}
/>
```

---

## Cover letter editor

The package also exports a full cover letter editor with live preview, template switching, and AI body rewriting.

### Quick start

```tsx
import { SmartCoverLetterEditor } from '@smart-resume/editor';
import '@smart-resume/editor/dist/styles.css';

export default function CoverLetterPage() {
  return (
    <div style={{ height: '100vh' }}>
      <SmartCoverLetterEditor
        coverLetter={myCoverLetter}
        onSave={(updated) => saveToDatabase(updated)}
      />
    </div>
  );
}
```

### CoverLetter data shape

```ts
interface CoverLetter {
  sender?: {
    name?: string;
    email?: string;
    phone?: string;
    address?: string;
    city?: string;
    region?: string;
  };
  recipient?: {
    name?: string;
    title?: string;
    company?: string;
    address?: string;
    city?: string;
    region?: string;
  };
  date?: string;
  subject?: string;
  salutation?: string;
  body: string;          // supports HTML from the rich text editor
  signOff?: string;
  meta?: {
    targetRole?: string;
    targetCompany?: string;
    lastModified?: string;
  };
}
```

### Props

#### Required

| Prop | Type | Description |
|------|------|-------------|
| `coverLetter` | `CoverLetter` | Initial cover letter data |
| `onSave` | `(coverLetter: CoverLetter) => void \| Promise<void>` | Called on every edit (auto-save) |

#### Optional

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `onManualSave` | `(coverLetter: CoverLetter) => void \| Promise<void>` | — | If provided, a Save button appears in the header |
| `template` | `CoverLetterTemplate` | `'classic'` | Controlled template (`'classic'` \| `'modern'` \| `'minimal'` \| `'executive'`) |
| `onTemplateChange` | `(t: CoverLetterTemplate) => void` | — | Called when user switches template |
| `onRewriteBody` | `(text, context) => Promise<string>` | — | AI body paragraph rewrite. Return the rewritten string |
| `healthChecks` | `HealthCheck[]` | — | Check items to display in the health panel |
| `healthSuggestions` | `AISuggestion[]` | — | AI suggestions to display in the health panel |
| `atsScore` | `number` | — | ATS score (0–100) shown in the health panel |
| `onRecalculateHealth` | `() => void \| Promise<void>` | — | Called when user clicks Re-calculate |
| `documentName` | `string` | — | Editable document name shown in the toolbar. Also used as PDF filename fallback |
| `onDocumentNameChange` | `(name: string) => void` | — | Called when the user renames the document in the toolbar |
| `pdfServerUrl` | `string` | — | URL of the PDF server |
| `pdfHeaders` | `Record<string, string>` | — | Extra headers for PDF generation |
| `pdfTitle` | `string` | — | Title for exported PDF metadata |
| `pdfFilename` | `string` | — | Filename for downloaded PDF (without `.pdf`). Takes precedence over `documentName` |
| `onExportPDF` | `(payload) => void \| Promise<void>` | — | Override entire PDF export flow |
| `onClose` | `() => void` | — | If provided, an x button appears in the top bar |
| `config` | `SreConfig` | — | Grouped config object (same as resume editor, country toggle is always hidden) |
| `className` | `string` | `''` | Extra CSS class on the root element |

### AI body rewrite

```tsx
<SmartCoverLetterEditor
  coverLetter={coverLetter}
  onSave={onSave}
  onRewriteBody={async (text, { targetRole, targetCompany }) => {
    const res = await fetch('/api/rewrite-paragraph', {
      method: 'POST',
      body: JSON.stringify({ text, targetRole, targetCompany }),
    });
    const { result } = await res.json();
    return result;
  }}
/>
```

---

## PDF export

Point `pdfServerUrl` at the PDF server. The Export PDF button appears automatically:

```tsx
<SmartResumeEditor
  resume={resume}
  onSave={onSave}
  pdfServerUrl={process.env.NEXT_PUBLIC_PDF_SERVER_URL}
/>
```

For local dev with `smart-resume-editor`, use env vars so you can switch between local and production without touching code:

```tsx
// src/pages/Index.tsx
<SmartResumeEditor
  resume={resume}
  onSave={onSave}
  pdfServerUrl={import.meta.env.VITE_PDF_SERVER_URL ?? 'http://localhost:3002'}
  pdfHeaders={{ 'x-internal-secret': import.meta.env.VITE_PDF_INTERNAL_SECRET ?? '' }}
/>
```

```env
# smart-resume-editor/.env.local
VITE_PDF_SERVER_URL=http://localhost:3002   # Hetzner dev instance via tunnel
VITE_PDF_INTERNAL_SECRET=your-secret

# Switch to prod by changing:
# VITE_PDF_SERVER_URL=https://pdf.jobjam.io
```

`pdfHeaders` lets you pass the internal secret directly from the browser — only safe for local dev. In production (`your-next-app`), use a server-side proxy route instead so the secret never reaches the browser.

### Local dev tunnels (two terminal tabs)

The PDF server on Hetzner needs to reach your local Vite app:

```bash
# Tab 1 — bring Hetzner's PDF dev server to your local machine
ssh -L 3002:localhost:3002 pdfserver@your-server-ip

# Tab 2 — expose your local Vite :8080 to the server
ssh -R 8080:localhost:8080 pdfserver@your-server-ip
```

Keep both open while testing.

### ResumePrintPage & CoverLetterPrintPage

The package exports print route components the PDF server navigates to when generating PDFs. Mount them in your app's router:

```tsx
import { ResumePrintPage, CoverLetterPrintPage } from '@smart-resume/editor';

// React Router
<Route path="/print" element={<ResumePrintPage />} />
<Route path="/print/cover-letter" element={<CoverLetterPrintPage />} />

// Next.js App Router
// app/print/page.tsx
import { ResumePrintPage } from '@smart-resume/editor';
export default function PrintPage() {
  return <ResumePrintPage />;
}

// app/print/cover-letter/page.tsx
import { CoverLetterPrintPage } from '@smart-resume/editor';
export default function CoverLetterPrint() {
  return <CoverLetterPrintPage />;
}
```

Deploy the PDF server and set `FRONTEND_URL` to your app's origin:

```
FRONTEND_URL=https://your-app.com
```

The flow:
1. User clicks Export PDF
2. `SmartResumeEditor` writes `{ resume, template, country, sectionOrder }` to `sessionStorage`
3. PDF server opens `{FRONTEND_URL}/print` in a headless browser (Puppeteer)
4. `ResumePrintPage` reads from `sessionStorage` and renders `ResumePreview` with `isPrint`
5. Server waits for `window.__RESUME_READY__ === true`, captures the PDF, streams it back

---

## Resume Comparison

The comparison module provides side-by-side diffing of resume versions with visual indicators for added, removed, and changed fields. Four components and two hooks cover read-only diffs, editable side-by-side views, and full-page overlays.

### `ResumeComparison`

Inline, collapsible diff panel with version selectors and a toggle button. Read-only.

```tsx
import { ResumeComparison } from '@smart-resume/editor';
import type { ResumeVersion } from '@smart-resume/editor';

const versions: ResumeVersion[] = [
  { id: 'original', label: 'Original', resume: originalResume, atsScore: 72 },
  { id: 'optimized', label: 'Optimized', resume: optimizedResume, atsScore: 88 },
];

<ResumeComparison
  versions={versions}
  enabled={true}
  onToggle={(on) => setEnabled(on)}
  defaultSectionsExpanded={false}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `versions` | `ResumeVersion[]` | required | Resume versions to compare |
| `enabled` | `boolean` | `undefined` | Controlled toggle state. Omit for uncontrolled |
| `onToggle` | `(enabled: boolean) => void` | — | Called when the user toggles comparison on/off |
| `defaultSectionsExpanded` | `boolean` | `false` | Whether section diffs start expanded |
| `className` | `string` | — | CSS class for the wrapper |

### `ResumeComparisonEditor`

Full-screen modal with two side-by-side `SmartResumeEditor` instances. Supports live editing, diff highlighting, inline comparison values, template/country switching, and PDF export.

```tsx
import { ResumeComparisonEditor } from '@smart-resume/editor';
import type { EditableResumeVersion } from '@smart-resume/editor';

const versions: EditableResumeVersion[] = [
  {
    id: 'original',
    label: 'Original',
    resume: originalResume,
    atsScore: 72,
    onSave: (resume) => save('original', resume),
    onManualSave: (resume, editorState) => manualSave('original', resume),
    onExportPDF: (payload) => exportPDF(payload),
  },
  {
    id: 'optimized',
    label: 'Optimized',
    resume: optimizedResume,
    atsScore: 88,
    onSave: (resume) => save('optimized', resume),
  },
];

<ResumeComparisonEditor
  versions={versions}
  open={isOpen}
  onClose={() => setIsOpen(false)}
  initialLeftId="original"
  initialRightId="optimized"
  template="classic"
  onTemplateChange={setTemplate}
  country="us"
  onCountryChange={setCountry}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `versions` | `EditableResumeVersion[]` | required | Editable resume versions |
| `open` | `boolean` | required | Whether the modal is visible |
| `onClose` | `() => void` | required | Close handler (also triggered by Escape) |
| `initialLeftId` | `string` | first version | Initial left (base) version id |
| `initialRightId` | `string` | last version | Initial right (target) version id |
| `template` | `ResumeTemplate` | — | Shared template for both editors |
| `onTemplateChange` | `(template) => void` | — | Called when user switches template |
| `country` | `CountryProfile` | — | Shared country profile |
| `onCountryChange` | `(country) => void` | — | Called when user switches country |
| `config` | `SreConfig` | — | Shared config passed to both editors |
| `defaultSectionsOpen` | `boolean` | `false` | Whether editor sections start expanded |

### `FullPageComparison`

Portal-based full-page overlay with two read-only `ResumePreview` renderings, synchronized scrolling, and zoom controls.

```tsx
import { FullPageComparison } from '@smart-resume/editor';

<FullPageComparison
  versions={versions}
  open={isOpen}
  onClose={() => setIsOpen(false)}
  template="modern"
  country="us"
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `versions` | `ResumeVersion[]` | required | Resume versions to compare |
| `open` | `boolean` | required | Whether the overlay is visible |
| `onClose` | `() => void` | required | Close handler |
| `initialLeftId` | `string` | first version | Initial left version id |
| `initialRightId` | `string` | last version | Initial right version id |
| `template` | `ResumeTemplate` | `'classic'` | Template for rendering previews |
| `country` | `CountryProfile` | `'us'` | Country profile for previews |
| `sectionOrder` | `string[]` | — | Custom section order |

### `InlineComparison`

Lightweight inline component (no portal/modal) with two read-only previews, zoom controls, and synchronized scrolling. Embeds directly in a page layout.

```tsx
import { InlineComparison } from '@smart-resume/editor';

<InlineComparison
  versions={versions}
  template="classic"
  country="us"
  height={700}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `versions` | `ResumeVersion[]` | required | Resume versions to compare |
| `initialLeftId` | `string` | first version | Initial left version id |
| `initialRightId` | `string` | last version | Initial right version id |
| `template` | `ResumeTemplate` | `'classic'` | Template for rendering previews |
| `country` | `CountryProfile` | `'us'` | Country profile for previews |
| `sectionOrder` | `string[]` | — | Custom section order |
| `height` | `number \| string` | `700` | Height of the comparison area |
| `className` | `string` | — | CSS class for the wrapper |

### Choosing a comparison component

| Need | Component |
|------|-----------|
| Collapsible diff panel inside an existing page | `ResumeComparison` |
| Side-by-side editing with live diffs | `ResumeComparisonEditor` |
| Full-page read-only preview overlay | `FullPageComparison` |
| Inline read-only previews (no overlay) | `InlineComparison` |

### Comparison hooks

#### `useResumeComparison(original, revised)`

Compares two `JsonResume` objects and returns a `ComparisonResult` with field-level diffs organized by section. Diffs: basics, work, education, skills, projects, volunteer. Array items are matched by `id`. Returns `null` when either input is `null`.

```tsx
import { useResumeComparison } from '@smart-resume/editor';

const result = useResumeComparison(originalResume, revisedResume);
// result.sections  — SectionDiff[]
// result.summary   — { totalAdded, totalRemoved, totalChanged, totalUnchanged }
```

#### `useDiffHighlightKeys(reference, target)`

Returns a `Set<string>` of field keys that differ between two resumes, compatible with `ResumePreview` / `ResumeEditor` highlighting.

```tsx
import { useDiffHighlightKeys } from '@smart-resume/editor';

const changedKeys = useDiffHighlightKeys(originalResume, optimizedResume);
// Set { 'basics.summary', 'work.abc123.highlights.0', 'skills.2', ... }
```

Key format: `basics.name`, `work.{id}.highlights.{index}`, `education.{id}`, `skills.{index}`, `projects.{id}.highlights.{index}`.

### Comparison types

```ts
interface ResumeVersion {
  id: string;
  label: string;
  resume: JsonResume;
  atsScore?: number;
}

interface EditableResumeVersion extends ResumeVersion {
  onSave: (resume: JsonResume) => void | Promise<void>;
  onManualSave?: (resume: JsonResume, editorState: EditorCustomization) => void | Promise<void>;
  onRewriteBullet?: (bullet: string, context: RewriteBulletContext) => Promise<string>;
  onRewriteBullets?: (bullets: string[], context: RewriteBulletContext) => Promise<string[]>;
  onRewriteSummary?: (summary: string, context: { jobTitle: string }) => Promise<string>;
  healthChecks?: HealthCheck[];
  healthSuggestions?: AISuggestion[];
  onRecalculateHealth?: () => void | Promise<void>;
  pdfServerUrl?: string;
  pdfHeaders?: Record<string, string>;
  onExportPDF?: (payload: { resume: JsonResume; template: ResumeTemplate; country: CountryProfile; sectionOrder: string[]; title?: string }) => void | Promise<void>;
}

type DiffStatus = 'added' | 'removed' | 'changed' | 'unchanged';

interface FieldDiff {
  field: string;
  label: string;
  status: DiffStatus;
  oldValue: string;
  newValue: string;
}

interface SectionDiff {
  section: string;
  label: string;
  fields: FieldDiff[];
  summary: { added: number; removed: number; changed: number };
}

interface ComparisonResult {
  sections: SectionDiff[];
  summary: { totalAdded: number; totalRemoved: number; totalChanged: number; totalUnchanged: number };
}
```

---

## Layout & page break behaviour

`ResumePreview` uses a two-pass render to paginate content accurately:

1. A hidden measurer div renders all blocks at exactly 816px wide and measures each block's `offsetHeight` (not `getBoundingClientRect`, which would be affected by ancestor CSS transforms)
2. Blocks are distributed across pages using those measurements against `USABLE_HEIGHT = 912px` (1056px page − 2x72px padding)
3. Section headings are kept with their first item (orphan prevention)

This means page breaks are deterministic and identical in the preview and the exported PDF regardless of screen size or zoom level.

---

## GDPR & data privacy

Resume data contains PII (name, email, phone, work history). A few things to be aware of:

**Don't use third-party PDF APIs.** Sending resume data to an external service makes you a data controller passing PII to a data processor. This requires a signed DPA with that vendor and disclosure in your privacy policy. Run the PDF server yourself.

**Deploy the PDF server in your own infrastructure.** Railway, Render, and Fly.io all support EU regions — keep data residency in Europe if your users are EU-based.

**The PDF server does not log request bodies.** The `/generate-pdf` endpoint only logs `template` and `country` — never the resume content. Error responses are also sanitised and do not expose internal details.

**Secure the PDF server endpoint.** By default the server only allows requests from `FRONTEND_URL` via CORS, but CORS is a browser control only. For production, add an internal secret or signed token to the request so the endpoint can't be called directly:

```js
// server/index.js — add this middleware
app.use('/generate-pdf', (req, res, next) => {
  if (req.headers['x-internal-secret'] !== process.env.INTERNAL_SECRET) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
});
```

```tsx
// SmartResumeEditor — pass the header via pdfServerUrl fetch (fork required)
// or use a proxy route in your app that adds the header server-side
```

The cleanest approach is a proxy route in your Next.js app (`/api/generate-pdf`) that adds the secret header and forwards to the PDF server — the browser never sees the secret.

---

```bash
# Run the dev app with HMR — source of truth during development
cd smart-resume-editor
npm run dev

# Build the package for publishing / consuming in another app
cd smart-resume-editor/packages/editor
npm run build
```

After building, reinstall in the consumer app:

```bash
cd your-next-app
rm -rf node_modules/@smart-resume/editor && npm install
```

---

## Package structure

```
packages/editor/
├── src/
│   ├── index.ts                      # Public exports
│   ├── types.ts                      # All TypeScript types
│   ├── SmartResumeEditor.tsx         # Resume editor main component
│   ├── SmartCoverLetterEditor.tsx    # Cover letter editor main component
│   ├── ResumePrintPage.tsx           # Resume print route component
│   ├── CoverLetterPrintPage.tsx      # Cover letter print route component
│   ├── hooks/
│   │   ├── useAiHighlights.ts        # AI bullet accept/reject state
│   │   ├── useCoverLetterAiHighlights.ts  # AI body rewrite state
│   │   ├── useResumeComparison.ts    # Core resume diffing engine
│   │   ├── useDiffHighlightKeys.ts   # Diff highlight key generation
│   │   ├── useComparisonValues.ts    # Inline diff reference values
│   │   ├── usePdfExport.ts           # Resume PDF export flow
│   │   ├── useCoverLetterPdfExport.ts # Cover letter PDF export flow
│   │   └── use-mobile.ts             # Mobile viewport detection
│   ├── templates/
│   │   └── index.tsx                 # Template registry (classic, modern, minimal, executive)
│   └── components/
│       ├── editor/
│       │   ├── comparison/
│       │   │   ├── ResumeComparison.tsx       # Inline collapsible diff panel
│       │   │   ├── ResumeComparisonEditor.tsx # Full-screen side-by-side editor
│       │   │   ├── FullPageComparison.tsx      # Full-page read-only overlay
│       │   │   ├── InlineComparison.tsx        # Inline read-only comparison
│       │   │   ├── ComparisonSection.tsx       # Section diff renderer
│       │   │   ├── ComparisonSummary.tsx       # Summary stats & ATS comparison
│       │   │   └── index.ts                   # Public exports
│       │   ├── ResumePreview.tsx      # Paginated resume preview renderer
│       │   ├── ResumeEditor.tsx       # Resume form editor
│       │   ├── ZoomablePreview.tsx    # Zoom controls wrapper
│       │   ├── HealthFeed.tsx         # Health panel
│       │   ├── ATSScoreGauge.tsx      # ATS score display
│       │   ├── AISuggestionCard.tsx   # Suggestion card with apply button
│       │   ├── TemplateToggle.tsx     # Template selector
│       │   ├── CountryToggle.tsx      # Country selector
│       │   └── EditableDocumentName.tsx # Inline-editable document name
│       └── cover-letter/
│           ├── CoverLetterPreview.tsx # Cover letter preview renderer
│           ├── CoverLetterEditor.tsx  # Cover letter form editor
│           ├── SenderSection.tsx      # Sender contact info
│           ├── RecipientSection.tsx   # Recipient details
│           ├── LetterMetaSection.tsx  # Date, subject, salutation
│           └── ParagraphsSection.tsx  # Rich text body editor
├── dist/                             # Built output (gitignored)
├── package.json
├── tsup.config.ts
├── tailwind.config.ts
└── postcss.config.cjs
```

---

## Storybook

Interactive component documentation is available via Storybook:

```bash
# Run Storybook locally
npm run storybook

# Build static Storybook site
npm run build-storybook
```

Storybook starts at **http://localhost:6006** and includes stories for all comparison components and hooks.

---

## Peer dependencies

- `react >= 18`
- `react-dom >= 18`