@unlev/exeq 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -0
- package/dist/embed.css +1 -0
- package/dist/embed.global.js +419 -0
- package/dist/index.css +567 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +105 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +1318 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1266 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +566 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Exeq
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @unlev/exeq
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### React Components
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { DesignerView, SignerView } from '@unlev/exeq';
|
|
17
|
+
import '@unlev/exeq/styles';
|
|
18
|
+
|
|
19
|
+
// Template editor — upload a PDF, place form fields, export template JSON
|
|
20
|
+
function Editor() {
|
|
21
|
+
return <DesignerView />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Signing UI — load a template, fill fields, download signed PDF
|
|
25
|
+
function Signer() {
|
|
26
|
+
return (
|
|
27
|
+
<SignerView
|
|
28
|
+
initialPdfUrl="/contracts/template.pdf"
|
|
29
|
+
initialTemplate={template}
|
|
30
|
+
initialSigner="Signer 1"
|
|
31
|
+
onComplete={(blob) => {
|
|
32
|
+
// upload the signed PDF
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Script Tag (CDN / iframe)
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<script src="https://unpkg.com/@unlev/exeq/dist/embed.global.js"></script>
|
|
43
|
+
<link rel="stylesheet" href="https://unpkg.com/@unlev/exeq/dist/styles.css" />
|
|
44
|
+
|
|
45
|
+
<div id="editor" style="width: 100%; height: 800px;"></div>
|
|
46
|
+
|
|
47
|
+
<script>
|
|
48
|
+
Exeq.editor({ target: '#editor', pdf: '/blank.pdf' });
|
|
49
|
+
</script>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### Components
|
|
55
|
+
|
|
56
|
+
| Component | Description |
|
|
57
|
+
|-----------|-------------|
|
|
58
|
+
| `DesignerView` | Full template editor — PDF upload, field placement, role assignment, template export |
|
|
59
|
+
| `SignerView` | Signing UI — displays PDF with fields for the signer to fill and sign |
|
|
60
|
+
| `PdfViewer` | Low-level PDF page renderer with draggable field overlays |
|
|
61
|
+
| `SignatureCanvas` | Freehand signature/initials drawing canvas |
|
|
62
|
+
| `FieldPropertyPanel` | Field property editor (type, label, dimensions, assignee) |
|
|
63
|
+
| `FieldNavigator` | Prev/Next navigation through signer fields |
|
|
64
|
+
| `SignerRoleSelector` | Manage signer roles (add, remove, switch active role) |
|
|
65
|
+
|
|
66
|
+
### `SignerView` Props
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Description |
|
|
69
|
+
|------|------|-------------|
|
|
70
|
+
| `initialPdfUrl` | `string` | URL to the PDF |
|
|
71
|
+
| `initialTemplate` | `Template` | Template object with fields and signer roles |
|
|
72
|
+
| `initialSigner` | `string` | Signer role name (default: `"Signer 1"`) |
|
|
73
|
+
| `callbackUrl` | `string` | URL to POST the signed PDF to |
|
|
74
|
+
| `onComplete` | `(blob: Blob) => void` | Callback with the signed PDF |
|
|
75
|
+
|
|
76
|
+
### Utilities
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { renderPdfPages, generateFilledPdf, downloadPdf } from '@unlev/exeq';
|
|
80
|
+
|
|
81
|
+
// Render PDF pages to images
|
|
82
|
+
const pages = await renderPdfPages(pdfUrlOrBytes);
|
|
83
|
+
|
|
84
|
+
// Generate a filled PDF with form values overlaid
|
|
85
|
+
const bytes = await generateFilledPdf(pdfSource, fields);
|
|
86
|
+
|
|
87
|
+
// Trigger browser download
|
|
88
|
+
downloadPdf(bytes, 'signed-document.pdf');
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Types
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import type { FormField, Template, FieldType, RenderedPage } from '@unlev/exeq';
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Field types:** `text`, `signature`, `initials`, `signed-date`, `checkbox`
|
|
98
|
+
|
|
99
|
+
**Text subtypes:** `freeform`, `number`, `date`, `email`, `phone`
|
|
100
|
+
|
|
101
|
+
### Template JSON
|
|
102
|
+
|
|
103
|
+
The template exported by the editor:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"pdfUrl": "https://example.com/contract.pdf",
|
|
108
|
+
"signerRoles": ["Sender", "Signer 1"],
|
|
109
|
+
"fields": [
|
|
110
|
+
{
|
|
111
|
+
"id": "uuid",
|
|
112
|
+
"type": "text",
|
|
113
|
+
"label": "Full Name",
|
|
114
|
+
"placeholder": "Enter your full name",
|
|
115
|
+
"required": true,
|
|
116
|
+
"assignee": "Signer 1",
|
|
117
|
+
"page": 0,
|
|
118
|
+
"x": 15.5,
|
|
119
|
+
"y": 42.3,
|
|
120
|
+
"width": 25,
|
|
121
|
+
"height": 3,
|
|
122
|
+
"fontSize": 12,
|
|
123
|
+
"value": ""
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Coordinates (`x`, `y`, `width`, `height`) are percentages of the page dimensions (0-100). Origin is top-left.
|
|
130
|
+
|
|
131
|
+
## Workflow
|
|
132
|
+
|
|
133
|
+
1. **Design** — Use `DesignerView` (or `Exeq.editor()`) to place fields on a PDF
|
|
134
|
+
2. **Pre-fill** — Fill Sender fields (company name, your signature, etc.)
|
|
135
|
+
3. **Export** — Save the template JSON and host the PDF
|
|
136
|
+
4. **Sign** — Embed `SignerView` (or `Exeq.sign()`) for end users to fill and sign
|
|
137
|
+
5. **Collect** — Receive the signed PDF via `onComplete` or `callbackUrl`
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Run the demo site
|
|
143
|
+
npm run dev
|
|
144
|
+
|
|
145
|
+
# Build the npm package
|
|
146
|
+
npm run build:lib
|
|
147
|
+
|
|
148
|
+
# Build everything (package + Next.js site)
|
|
149
|
+
npm run build
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
package/dist/embed.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.designer-layout,.signer-layout{display:flex;flex-direction:column;height:100vh;font-family:system-ui,-apple-system,sans-serif;color:#222;background:#fafafa}.designer-toolbar{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #e5e5e5;background:#fff;gap:1rem;flex-wrap:wrap}.toolbar-left,.toolbar-right{display:flex;align-items:center;gap:.5rem}.designer-content,.signer-content{display:flex;flex:1;overflow:hidden}.designer-pdf-area,.signer-pdf-area{flex:1;overflow-y:auto;padding:1rem;background:#eee}.designer-panel,.signer-panel{width:320px;overflow-y:auto;border-left:1px solid #e5e5e5;background:#fff;padding:1rem;display:flex;flex-direction:column;gap:1rem}.pdf-viewer{display:flex;flex-direction:column;gap:1rem;align-items:center}.pdf-page{position:relative;width:100%;max-width:800px;background:#fff;box-shadow:0 1px 4px #0000001a;cursor:crosshair}.pdf-page-image{width:100%;height:100%;display:block;pointer-events:none}.field-overlay{position:absolute;border:1.5px solid;border-radius:2px;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:box-shadow .1s}.field-overlay.selected{box-shadow:0 0 0 2px #0003;z-index:10}.field-overlay.editable{cursor:text}.field-overlay-label{position:absolute;top:-18px;left:-1px;font-size:10px;color:#fff;padding:1px 6px;border-radius:2px 2px 0 0;white-space:nowrap;pointer-events:none}.field-overlay-placeholder{color:#999;font-size:11px;padding:2px 4px;width:100%;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.field-overlay-placeholder.readonly{color:#666}.field-overlay-value{font-size:11px;padding:2px 4px;width:100%}.field-inline-input{width:100%;height:100%;border:none;background:transparent;padding:2px 4px;outline:none;font-family:inherit}.field-signature-preview{width:100%;height:100%;object-fit:contain}.field-signature-filled{width:100%;height:100%;cursor:pointer}.field-checkbox-display{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700}.field-checkbox-display.editable{cursor:pointer}.field-resize-handle{position:absolute;bottom:0;right:0;width:10px;height:10px;background:#fff;border:1px solid #999;cursor:nwse-resize}.upload-btn{display:inline-block;padding:.4rem 1rem;background:#111;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:.85rem;font-weight:500}.upload-btn:hover{background:#333}.upload-btn-small{font-size:.8rem;padding:.3rem .75rem;background:#555}.upload-btn-large{font-size:1rem;padding:.6rem 1.5rem}.export-btn{padding:.4rem 1rem;background:#111;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:.85rem;font-weight:500}.export-btn:hover{background:#333}.field-type-selector{display:flex;gap:2px}.field-type-btn{padding:.3rem .6rem;background:#fff;border:1px solid #ddd;border-radius:4px;cursor:pointer;font-size:.8rem;color:#555}.field-type-btn.active{background:#111;color:#fff;border-color:#111}.field-type-btn:hover:not(.active){background:#f5f5f5}.signer-role-selector{padding:.5rem 1rem;border-bottom:1px solid #e5e5e5;background:#fff}.signer-role-label{font-size:.75rem;color:#888;margin-bottom:.35rem}.signer-role-list{display:flex;gap:.35rem;flex-wrap:wrap;align-items:center}.signer-role-chip{padding:.2rem .6rem;border:1.5px solid;border-radius:4px;cursor:pointer;font-size:.8rem;font-weight:500;background:transparent;transition:all .1s}.signer-role-remove{margin-left:.3rem;cursor:pointer;font-size:1rem}.signer-role-add-btn{padding:.2rem .5rem;border:1px dashed #ccc;border-radius:4px;background:none;cursor:pointer;font-size:.8rem;color:#888}.signer-role-add-input{display:flex;gap:.25rem;align-items:center}.signer-role-add-input input{padding:.2rem .4rem;border:1px solid #ccc;border-radius:4px;font-size:.8rem;width:120px}.signer-role-add-input button{padding:.2rem .5rem;border:1px solid #ccc;border-radius:4px;background:#fff;cursor:pointer;font-size:.8rem}.field-property-panel{display:flex;flex-direction:column;gap:.75rem}.panel-header{display:flex;justify-content:space-between;align-items:center}.panel-header h3{margin:0;font-size:1rem}.panel-delete-btn{padding:.2rem .5rem;background:none;border:1px solid #e44;color:#e44;border-radius:4px;cursor:pointer;font-size:.8rem}.panel-delete-btn:hover{background:#e44;color:#fff}.panel-field label{display:block;font-size:.75rem;color:#888;margin-bottom:.2rem}.panel-field input[type=text],.panel-field input[type=number],.panel-field input[type=email],.panel-field input[type=tel],.panel-field input[type=date],.panel-field select{width:100%;padding:.35rem .5rem;border:1px solid #ddd;border-radius:4px;font-size:.85rem;background:#fff}.panel-field-group{display:grid;grid-template-columns:1fr 1fr;gap:.5rem}.panel-checkbox-label{display:flex;align-items:center;gap:.4rem;font-size:.85rem;cursor:pointer}.panel-empty{color:#888;font-size:.85rem;padding:2rem 1rem;text-align:center}.prefill-section{border-top:1px solid #eee;padding-top:.75rem}.prefill-section h4{margin:0 0 .5rem;font-size:.85rem;color:#555}.prefill-input{width:100%;padding:.35rem .5rem;border:1px solid #ddd;border-radius:4px;font-size:.85rem}.field-list{border-top:1px solid #eee;padding-top:.75rem}.field-list h4{margin:0 0 .5rem;font-size:.85rem;color:#555}.field-list-item{display:flex;align-items:center;gap:.4rem;padding:.35rem .5rem;border-radius:4px;cursor:pointer;font-size:.8rem}.field-list-item:hover{background:#f5f5f5}.field-list-item.active{background:#eee}.field-list-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.field-list-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field-list-page{color:#aaa;font-size:.75rem}.field-list-required{color:#e44;font-weight:700}.signer-field-input h3{margin:0 0 .5rem;font-size:1rem}.required-badge{display:inline-block;font-size:.7rem;background:#fee;color:#c33;padding:1px 6px;border-radius:3px;margin-bottom:.5rem}.signer-text-input{width:100%;padding:.4rem .5rem;border:1px solid #ddd;border-radius:4px;font-size:.9rem}.signer-checkbox-label{display:flex;align-items:center;gap:.5rem;font-size:.9rem;cursor:pointer}.signer-date-display{font-size:.9rem;color:#555;padding:.4rem 0}.signer-field-readonly{color:#888;font-size:.85rem}.signer-field-readonly h3{margin:0 0 .25rem;font-size:1rem;color:#555}.signature-canvas-wrapper{display:flex;flex-direction:column;gap:.35rem}.signature-canvas{border:1px solid #ddd;border-radius:4px;background:#fff;cursor:crosshair}.signature-canvas-actions{display:flex;align-items:center;justify-content:space-between}.signature-clear-btn{padding:.2rem .5rem;background:none;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:.8rem;color:#888}.signature-hint{font-size:.75rem;color:#aaa}.field-navigator{margin-top:auto;border-top:1px solid #eee;padding-top:.75rem;display:flex;flex-direction:column;gap:.5rem}.field-navigator-progress{font-size:.8rem;color:#888;text-align:center}.field-navigator-controls{display:flex;align-items:center;justify-content:center;gap:1rem}.nav-btn{padding:.35rem 1rem;border:1px solid #ddd;border-radius:4px;background:#fff;cursor:pointer;font-size:.85rem}.nav-btn:hover:not(:disabled){background:#f5f5f5}.nav-btn:disabled{opacity:.4;cursor:default}.field-navigator-position{font-size:.85rem;color:#888;min-width:50px;text-align:center}.submit-btn{padding:.5rem;background:#111;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:.9rem;font-weight:500}.submit-btn:hover:not(:disabled){background:#333}.submit-btn:disabled{opacity:.4;cursor:default}.loading-indicator{text-align:center;padding:2rem;color:#888;font-size:.9rem}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:.75rem;color:#888}.empty-state h2{margin:0;font-size:1.25rem;color:#555}.empty-state p{margin:0;font-size:.9rem}.powered-by{text-align:center;padding:.4rem;font-size:.7rem;color:#aaa;border-top:1px solid #eee;background:#fff}.powered-by a{color:#888;text-decoration:none}.powered-by a:hover{text-decoration:underline}
|