npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@smart-resume/editor

v1.4.0

Published

React resume editor and cover letter editor with live preview, templates, ATS scoring, and PDF export

Readme

@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-compatible object — the component handles editing, live preview, template switching, country profiles, zoom, and an optional AI health panel.


Installation

npm install @smart-resume/editor

Import the stylesheet once in your app entry point:

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

Quick start

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:

<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):

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

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

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)

// 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

<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:

<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

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

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

<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:

<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:

// 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 ?? '' }}
/>
# 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:

# 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:

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.

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.

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.

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.

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.

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.

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

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:

// 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();
});
// 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.


# 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:

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:

# 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