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

@unlev/exeq

v0.6.4

Published

Embeddable PDF form builder and document signing — client-side, no backend required

Readme

Exeq

Embeddable PDF form builder and document signing for the browser. Design form templates on any PDF, then let users fill and sign them — all client-side, no backend required. Your documents never leave the device.

Install

npm install @unlev/exeq
# or
yarn add @unlev/exeq

Quick Start

import { DesignerView, SignerView } from '@unlev/exeq';
import '@unlev/exeq/styles';

// Template editor — open a PDF, place fields, export template
function Editor() {
  return (
    <DesignerView
      apiKey="your-api-key"
      initialPdfUrl="/contracts/blank.pdf"
      onSave={(template) => {
        // store the template JSON on your server
      }}
    />
  );
}

// Signing UI — load a template, pre-fill values, collect signed PDF
function Signer() {
  return (
    <SignerView
      apiKey="your-api-key"
      initialPdfUrl="/contracts/template.pdf"
      initialTemplate={template}
      initialSigner="Signer 1"
      initialValues={{
        "Full Name": "Jane Smith",
        "Email": "[email protected]",
      }}
      onComplete={(blob) => {
        // upload the signed PDF
      }}
    />
  );
}

// Multi-party signing — signers complete sequentially
function MultiPartySigner() {
  return (
    <SignerView
      apiKey="your-api-key"
      initialPdfUrl="/contracts/template.pdf"
      initialTemplate={template}
      signerOrder={['Signer 1', 'Sender']}  // recipient signs first, then sender
      onComplete={(blob) => {
        // final PDF with all signatures
      }}
    />
  );
}

API

DesignerView Props

| Prop | Type | Required | Description | |------|------|----------|-------------| | apiKey | string | Yes | Your Exeq API key | | initialPdfUrl | string | No | URL to a PDF to pre-load | | initialTemplate | Template | No | Existing template to resume editing | | onSave | (template: Template) => void | No | Called on "Export Template". If omitted, downloads JSON. | | onChange | (template: Template) => void | No | Fires on every change to fields / signer roles / loaded PDF. Use for draft persistence (e.g. localStorage) so accidental refresh or tab close doesn't lose work. |

SignerView Props

| Prop | Type | Required | Description | |------|------|----------|-------------| | apiKey | string | Yes | Your Exeq API key | | initialPdfUrl | string | No | URL to the PDF | | initialTemplate | Template | No | Template with fields and signer roles | | initialSigner | string | No | Signer role name (default: "Signer 1") | | initialValues | Record<string, string> | No | Pre-fill fields by label (case-insensitive) or ID | | callbackUrl | string | No | URL to POST the signed PDF to | | onComplete | (blob: Blob) => void | No | Callback with the signed PDF Blob | | submitLabel | string | No | Label for the final submit button (default: "Complete") | | signerOrder | string[] | No | Signing order for multi-party docs. Signers complete sequentially. Defaults to non-Sender roles first, then Sender. | | transforms | Record<string, (value: string) => string> | No | Custom transform functions for formula fields. Merges with built-in transforms. |

Formulas

Fields can reference other fields and apply transforms using the syntax {{Source Field Label | transform}}. Formula fields auto-compute their values and are read-only to the signer.

// In the template designer, set a field's formula to:
// {{Contract Date | month2}}/{{Contract Date | day2}}/{{Contract Date | year}}
// {{Full Name | first}}
// {{SSN | last4}}

// Pass custom transforms via the SignerView prop:
<SignerView
  initialTemplate={template}
  transforms={{
    'last4': (value) => value.slice(-4),
    'ssn-masked': (value) => `***-**-${value.slice(-4)}`,
    'currency': (value) => `$${parseFloat(value).toFixed(2)}`,
    'uppercase': (value) => value.toUpperCase(),
  }}
/>

Built-in transforms:

| Transform | Description | Example | |-----------|-------------|---------| | month | Numeric month (1-12) | 3 | | month2 | Zero-padded month | 03 | | monthname | Full month name | March | | monthshort | Short month name | Mar | | day | Day of month | 5 | | day2 | Zero-padded day | 05 | | year | 4-digit year | 2026 | | year2 | 2-digit year | 26 | | upper | Uppercase | JANE SMITH | | lower | Lowercase | jane smith | | first | First word | Jane | | last | Last word | Smith | | initials | First letter of each word | JS | | last4 | Last 4 characters | 6789 | | last2 | Last 2 characters | 89 | | first4 | First 4 characters | 1234 | | first2 | First 2 characters | 12 | | digits | Digits only | 1234567890 | | number | Parse as number | 42 | | currency | Format as USD | $42.00 | | trim | Remove whitespace | text |

Custom transforms override built-ins with the same name. Any transform name is valid — use whatever makes sense for your use case.

Utilities

import { renderPdfPages, generateFilledPdf, downloadPdf } from '@unlev/exeq';

// Render PDF pages to images
const pages = await renderPdfPages(pdfUrlOrBytes);

// Generate a filled PDF
const bytes = await generateFilledPdf({ pdfSource, fields });

// Trigger browser download
downloadPdf(bytes, 'signed-document.pdf');

generateFilledPdf

generateFilledPdf(opts: FillPdfOptions): Promise<Uint8Array>
import { generateFilledPdf, US_LETTER } from '@unlev/exeq';

// Overlay-only: blank Letter pages with no background — for printing
// onto pre-printed physical forms.
const overlay = await generateFilledPdf({
  pdfSource: null,
  fields,
  pageSize: US_LETTER,
});

// Always-Letter output even when the source PDF isn't Letter — useful
// when the source is a scan at slightly different dimensions.
const letter = await generateFilledPdf({
  pdfSource: '/forms/template.pdf',
  fields,
  pageSize: US_LETTER,
});

// Resolve formula fields during render.
const filled = await generateFilledPdf({
  pdfSource,
  fields,
  resolveFormulas: true,
  customTransforms: { last4: v => v.slice(-4) },
});

| Option | Type | Description | |--------|------|-------------| | pdfSource | string \| ArrayBuffer \| null | Background PDF (URL or bytes), or null for overlay-only output. | | fields | FormField[] | Fields to render on top of the background. | | pageSize | [number, number] | Override output page size in PDF points (72pt = 1in). With a source, source pages are drawn stretched to fit. Defaults to source dims, or Letter if no source. | | pageCount | number | When pdfSource is null, how many blank pages to render. Default 1. | | resolveFormulas | boolean | Run resolveAllFormulas on fields before rendering. Default false. | | customTransforms | TransformMap | Merged with BUILTIN_TRANSFORMS during formula resolution. | | calibration | Calibration | Linear offset/scale applied to field positions. |

createPdfBuilder — batch / mail merge

For multi-record output (e.g. one PDF with one page per recipient), createPdfBuilder merges records into a single document and dedupes shared resources — the source PDF, fonts, and signature PNGs are embedded once and referenced from every page.

import { createPdfBuilder, downloadPdf, US_LETTER } from '@unlev/exeq';

const builder = await createPdfBuilder({ pageSize: US_LETTER });
for (const record of records) {
  await builder.addRecord({
    pdfSource: '/forms/template.pdf',  // embedded once, referenced N times
    fields: record.fields,
    resolveFormulas: true,
  });
}
const bytes = await builder.save();
downloadPdf(bytes, 'merged.pdf');

A 12-record batch with a 700 KB background goes from ~8 MB (12 separate generateFilledPdf calls, then merged with pdf-lib) to ~705 KB (one embed shared across pages).

PdfBuilder.doc exposes the underlying pdf-lib document if you need to add an audit trail or table-of-contents page before saving.

Calibration

When a printed overlay doesn't quite register with a pre-printed physical form (because the scan was cropped differently than the real paper, or the printer tray has a small offset), apply a linear calibration. Offsets are in PDF points (72pt = 1 inch).

import { applyCalibration, generateFilledPdf, US_LETTER } from '@unlev/exeq';

// Either pass calibration to the renderer:
const bytes = await generateFilledPdf(pdfSource, fields, {
  pageSize: US_LETTER,
  calibration: { xOffset: -5, yOffset: 3, xScale: 1.005, yScale: 1 },
});

// …or apply it as a standalone transform (useful for previews):
const adjusted = applyCalibration(fields, { xOffset: -5, yOffset: 3, xScale: 1.005, yScale: 1 });

| Field | Type | Meaning | |-------|------|---------| | xOffset | number (pt) | Horizontal shift; positive = right. | | yOffset | number (pt) | Vertical shift; positive = down (screen coords). | | xScale | number | Multiplier on x and width. | | yScale | number | Multiplier on y and height. |

Page-size constants

import { US_LETTER, US_LEGAL, A4 } from '@unlev/exeq';
// US_LETTER === [612, 792]
// US_LEGAL  === [612, 1008]
// A4        === [595.28, 841.89]

Types

import type { FormField, Template, FieldType, RenderedPage } from '@unlev/exeq';

Field Types

| Type | Description | |------|-------------| | text | Text input (subtypes: freeform, number, date, email, phone) | | signature | Freehand signature drawing (supports ink color) | | initials | Smaller freehand drawing for initials | | signed-date | Auto-filled date when signer signs | | checkbox | Toggle checkbox | | blackout | Black redaction rectangle (designer only) | | whiteout | White redaction rectangle (designer only) |

Additional Components

| Component | Description | |-----------|-------------| | PdfViewer | Low-level PDF page renderer with draggable field overlays | | SignatureCanvas | Freehand signature/initials drawing canvas | | FieldPropertyPanel | Field property editor (type, label, assignee) | | FieldNavigator | Prev/Next navigation through signer fields | | SignerRoleSelector | Manage signer roles |

Privacy

All PDF processing happens client-side in the browser. Documents are never uploaded to any server. The npm package is self-hosted — it bundles into your app and serves from your infrastructure.

Workflow

  1. Design — Use <DesignerView /> to open a PDF and place form fields
  2. Pre-fill — Fill Sender fields (company name, your signature, etc.)
  3. Export — Capture the template JSON via onSave
  4. Sign — Render <SignerView /> with the template. Use initialValues for mail-merge.
  5. Collect — Receive the signed PDF via onComplete

CDN Usage

For non-React apps, use the embed script:

<script src="https://unpkg.com/@unlev/exeq/dist/embed.global.js"></script>

<div id="signer" style="width:100%;height:800px"></div>
<script>
  Exeq.sign({
    target: '#signer',
    apiKey: 'your-api-key',
    pdf: '/contract.pdf',
    fields: '/template.json',
    onComplete: (blob) => { /* signed PDF */ }
  });
</script>

Documentation

Full docs at exeq.org/docs

License

MIT — Unleavened LLC