formlift 0.0.0-stage → 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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +481 -2
- package/dist/chunk-5VHPTLLV.cjs +1517 -0
- package/dist/chunk-5VHPTLLV.cjs.map +1 -0
- package/dist/chunk-7NZPYWL3.cjs +291 -0
- package/dist/chunk-7NZPYWL3.cjs.map +1 -0
- package/dist/chunk-BQAVPA7Q.js +1517 -0
- package/dist/chunk-BQAVPA7Q.js.map +1 -0
- package/dist/chunk-QCCWXAH3.js +291 -0
- package/dist/chunk-QCCWXAH3.js.map +1 -0
- package/dist/form-logic-DT9Bfwfz.d.cts +122 -0
- package/dist/form-logic-DT9Bfwfz.d.ts +122 -0
- package/dist/index.cjs +79 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +164 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/layout-ExhBeO6b.d.cts +27 -0
- package/dist/layout-ExhBeO6b.d.ts +27 -0
- package/dist/react/index.cjs +3023 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +162 -0
- package/dist/react/index.d.ts +162 -0
- package/dist/react/index.js +3023 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server/index.cjs +207 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +15 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.js +207 -0
- package/dist/server/index.js.map +1 -0
- package/dist/style.css +2 -0
- package/package.json +116 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-11-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `formlift` core: form model, validation, conditional logic, layout and PDF field auto-detection.
|
|
13
|
+
- `formlift/react`: `FormEditor` (builder) and `FormFiller` (fill-in view with list mode, autosave, signatures,
|
|
14
|
+
file uploads).
|
|
15
|
+
- `formlift/server`: `completedPdf()` with answers, signatures and embedded attachments.
|
|
16
|
+
- `formlift/style.css`: styles scoped to `.fl-root`, themeable with `--fl-*` variables.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Harendra Singh Negi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,482 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# formlift
|
|
4
|
+
|
|
5
|
+
**Turn any PDF into a fillable web form, with no AI and no server-side conversion.**
|
|
6
|
+
|
|
7
|
+
Auto-detect fields · Drag-and-drop builder · Conditional logic · Validation · Fill-in UI · Completed PDF
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/formlift)
|
|
10
|
+
[](https://www.npmjs.com/package/formlift)
|
|
11
|
+
[](https://www.npmjs.com/package/formlift)
|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
[](https://github.com/harendra-singh-negi/formlift/actions/workflows/ci.yml)
|
|
14
|
+
|
|
15
|
+
**[▶ Live demo](https://harendra-singh-negi.github.io/formlift/)** · [Quick start](#quick-start) · [API](#api-reference) · [Server](#server-side) · [Theming](#theming) · [FAQ](#faq)
|
|
16
|
+
|
|
17
|
+
<img src="./docs/builder.png" alt="formlift builder: suggested fields over a PDF page" width="860" />
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Why formlift?
|
|
24
|
+
|
|
25
|
+
Most organisations already have their forms as PDFs: inspection checklists, onboarding sheets, permits, audit
|
|
26
|
+
reports. Rebuilding each one in a form tool takes days and loses the original layout.
|
|
27
|
+
|
|
28
|
+
**formlift keeps the PDF as it is** and puts real form fields on top of it:
|
|
29
|
+
|
|
30
|
+
1. **Upload** a PDF. formlift reads the page drawing and **suggests fields** for dotted lines, underlines,
|
|
31
|
+
checkboxes, `____` blanks and empty table cells.
|
|
32
|
+
2. **Adjust** them in the builder: rename, retype, move, add rules and conditions.
|
|
33
|
+
3. **Publish.** People fill the form on the original page, on desktop or phone.
|
|
34
|
+
4. **Export** a completed PDF with the answers, signatures and attachments.
|
|
35
|
+
|
|
36
|
+
The original file is never modified. The whole form is plain JSON that you store wherever you like.
|
|
37
|
+
|
|
38
|
+
## Live demo
|
|
39
|
+
|
|
40
|
+
**[Try it in your browser →](https://harendra-singh-negi.github.io/formlift/)**
|
|
41
|
+
|
|
42
|
+
The demo runs the same components you install from npm:
|
|
43
|
+
|
|
44
|
+
1. **Build**: open a sample PDF or upload your own, review the suggested fields and edit them.
|
|
45
|
+
2. **Fill**: answer the form on the page or in the list view, then download the completed PDF.
|
|
46
|
+
3. **Code**: see the exact JSON your app receives from `onSave` and the code that wires it up.
|
|
47
|
+
|
|
48
|
+
Nothing is uploaded. Your PDF and answers stay in your browser. To run the demo locally, use `npm run dev`.
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
| | |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| 🔍 **Auto-detect** | Answer lines, checkboxes, typed blanks and table cells come from the PDF's own vector drawing. Sizes are measured relative to the text, so it works at any font size, scale or orientation. |
|
|
55
|
+
| 🧱 **Builder** | 11 field types, drag/resize/snap-to-grid, undo/redo, keyboard shortcuts, fill order, page reorder/insert/delete |
|
|
56
|
+
| ✏️ **Layout editing** | Add text, boxes and lines, or click any line of the original text to replace it, all without touching the file |
|
|
57
|
+
| 🔀 **Conditional logic** | `visibleWhen` / `requiredWhen` / `readOnlyWhen` with nested AND/OR groups and 10 operators. Circular rules are rejected. |
|
|
58
|
+
| ✅ **Validation** | Required, length, min/max, regex, email, phone, date and options. The **same code** runs in the browser and on your server. |
|
|
59
|
+
| 📱 **Filler** | Fields stay pinned to the page at every zoom, plus a list view with search and filters for phones. Includes autosave, signatures and file uploads. |
|
|
60
|
+
| 📄 **Completed PDF** | Answers, signature images and embedded attachments in a new PDF. Runs in Node, Bun, Deno, workers or the browser. |
|
|
61
|
+
| 🎨 **Isolated styling** | All CSS is scoped to `.fl-root` with no global reset, so it never clashes with your app (Tailwind included). Theme it with CSS variables. |
|
|
62
|
+
| 🧩 **Headless core** | Zero-dependency core you can use with any framework, or with none |
|
|
63
|
+
| 🟦 **TypeScript** | Written in TypeScript. Ships ESM, CommonJS and full type definitions. |
|
|
64
|
+
|
|
65
|
+
<div align="center">
|
|
66
|
+
<img src="./docs/filler.png" alt="formlift filler: answering fields on the original page" width="860" />
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
## Packages
|
|
70
|
+
|
|
71
|
+
One install, four entry points. Import only what you use:
|
|
72
|
+
|
|
73
|
+
| Entry | Contents | Needs |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `formlift` | Types, validation, conditions, auto-detect, helpers | – |
|
|
76
|
+
| `formlift/react` | `<FormEditor>`, `<FormFiller>`, `<PdfPages>`, `setPdfWorker` | `react`, `react-dom`, `pdfjs-dist`, `lucide-react` |
|
|
77
|
+
| `formlift/server` | `completedPdf()` | `pdf-lib` |
|
|
78
|
+
| `formlift/style.css` | Styles for the React components | – |
|
|
79
|
+
|
|
80
|
+
## Installation
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install formlift
|
|
84
|
+
# or
|
|
85
|
+
pnpm add formlift
|
|
86
|
+
# or
|
|
87
|
+
yarn add formlift
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Or install straight from GitHub (it builds on install), from a release tag or from a branch:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install github:harendra-singh-negi/formlift#v0.1.0
|
|
94
|
+
npm install github:harendra-singh-negi/formlift # latest main
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then add the peer dependencies for the parts you use:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install react react-dom pdfjs-dist lucide-react # React UI
|
|
101
|
+
npm install pdf-lib # completed PDF
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Requirements:** React 18 or 19 · pdfjs-dist 4+ · Node 18+ · browsers with CSS `@scope` support (Chrome/Edge 118+,
|
|
105
|
+
Safari 17.4+, Firefox 128+).
|
|
106
|
+
|
|
107
|
+
## Quick start
|
|
108
|
+
|
|
109
|
+
### 1. Configure pdf.js (once, at app start)
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { setPdfWorker } from 'formlift/react'
|
|
113
|
+
import 'formlift/style.css'
|
|
114
|
+
|
|
115
|
+
// Vite
|
|
116
|
+
import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
|
|
117
|
+
setPdfWorker(workerUrl)
|
|
118
|
+
|
|
119
|
+
// webpack 5 / Next.js
|
|
120
|
+
// setPdfWorker(new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString())
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> If you skip this step, the worker is loaded from unpkg.com for the installed pdfjs-dist version. That works,
|
|
124
|
+
> but it needs internet access and is blocked by a strict CSP.
|
|
125
|
+
|
|
126
|
+
### 2. Build a form (admin)
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { FormEditor, type FormDoc } from 'formlift/react'
|
|
130
|
+
|
|
131
|
+
export function FormBuilder({ pdfUrl, saved }: { pdfUrl: string; saved?: FormDoc }) {
|
|
132
|
+
return (
|
|
133
|
+
<div style={{ height: '100vh' }}>
|
|
134
|
+
<FormEditor
|
|
135
|
+
pdf={pdfUrl}
|
|
136
|
+
initialValue={saved}
|
|
137
|
+
title="Crew onboarding"
|
|
138
|
+
onSave={(doc) =>
|
|
139
|
+
fetch('/api/forms/42', {
|
|
140
|
+
method: 'PUT',
|
|
141
|
+
headers: { 'Content-Type': 'application/json' },
|
|
142
|
+
body: JSON.stringify(doc),
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 3. Fill it (users)
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { FormFiller, type FormDoc } from 'formlift/react'
|
|
155
|
+
|
|
156
|
+
export function FillForm({ pdfUrl, form }: { pdfUrl: string; form: FormDoc }) {
|
|
157
|
+
return (
|
|
158
|
+
<div style={{ height: '100vh' }}>
|
|
159
|
+
<FormFiller
|
|
160
|
+
pdf={pdfUrl}
|
|
161
|
+
fields={form.fields}
|
|
162
|
+
layout={form.layout}
|
|
163
|
+
onSave={async (answers, { submit }) => {
|
|
164
|
+
const res = await fetch('/api/forms/42/answers', {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: { 'Content-Type': 'application/json' },
|
|
167
|
+
body: JSON.stringify({ answers, submit }),
|
|
168
|
+
})
|
|
169
|
+
// Throw { errors: { fieldKey: message } } and the messages appear at the fields
|
|
170
|
+
if (!res.ok) throw await res.json()
|
|
171
|
+
}}
|
|
172
|
+
onSubmitted={() => alert('Thank you!')}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
That's it. Both components fill the height of their container.
|
|
180
|
+
|
|
181
|
+
## Server side
|
|
182
|
+
|
|
183
|
+
**Never trust the browser.** Run the same checks on your server with the core package:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import express from 'express'
|
|
187
|
+
import { cleanAnswers, validateAnswers, validateFields, validateLayout } from 'formlift'
|
|
188
|
+
import { completedPdf } from 'formlift/server'
|
|
189
|
+
|
|
190
|
+
const app = express().use(express.json({ limit: '5mb' }))
|
|
191
|
+
|
|
192
|
+
// Save a form definition
|
|
193
|
+
app.put('/api/forms/:id', async (req, res) => {
|
|
194
|
+
const { fields, layout } = req.body
|
|
195
|
+
const pageCount = await pagesOfUploadedPdf(req.params.id)
|
|
196
|
+
const problems = [...validateFields(fields, pageCount), ...(layout ? validateLayout(layout, pageCount) : [])]
|
|
197
|
+
if (problems.length) return res.status(400).json({ problems })
|
|
198
|
+
await db.forms.update(req.params.id, { fields, layout })
|
|
199
|
+
res.sendStatus(204)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// Save or submit answers
|
|
203
|
+
app.post('/api/forms/:id/answers', async (req, res) => {
|
|
204
|
+
const form = await db.forms.get(req.params.id)
|
|
205
|
+
const answers = cleanAnswers(form.fields, req.body.answers) // drops unknown keys, coerces types
|
|
206
|
+
if (req.body.submit) {
|
|
207
|
+
const errors = validateAnswers(form.fields, answers) // {} = valid; hidden fields are skipped
|
|
208
|
+
if (Object.keys(errors).length) return res.status(422).json({ errors })
|
|
209
|
+
}
|
|
210
|
+
await db.submissions.save(req.params.id, answers, req.body.submit)
|
|
211
|
+
res.sendStatus(204)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// Download the completed PDF
|
|
215
|
+
app.get('/api/submissions/:id/pdf', async (req, res) => {
|
|
216
|
+
const { form, answers, files } = await db.submissions.full(req.params.id)
|
|
217
|
+
const pdf = await completedPdf(form.originalPdf, form.fields, answers, files, form.layout)
|
|
218
|
+
res.type('application/pdf').send(Buffer.from(pdf))
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`files` maps a file answer's id to `{ fileName, fileType, bytes }`. Signatures are drawn as images and
|
|
223
|
+
uploads are embedded as PDF attachments.
|
|
224
|
+
|
|
225
|
+
## API reference
|
|
226
|
+
|
|
227
|
+
### `<FormEditor>`
|
|
228
|
+
|
|
229
|
+
| Prop | Type | Default | Description |
|
|
230
|
+
| --- | --- | --- | --- |
|
|
231
|
+
| `pdf` | `PdfSource` | **required** | The PDF: a URL, `Blob`/`File`, `ArrayBuffer`, `Uint8Array`, or an (async) function that returns one |
|
|
232
|
+
| `onSave` | `(doc: FormDoc) => unknown` | **required** | Called on Save. Throw or reject to keep the changes marked unsaved. |
|
|
233
|
+
| `initialValue` | `FormDoc` | empty | The saved form. To load a different one, remount with a new `key`. |
|
|
234
|
+
| `onChange` | `(doc: FormDoc) => void` | – | Called on every change, including undo and redo |
|
|
235
|
+
| `autoDetect` | `boolean` | new forms only | Runs field detection when the PDF opens |
|
|
236
|
+
| `title` | `ReactNode` | – | Toolbar title |
|
|
237
|
+
| `onBack` | `() => void` | – | Shows a Back button |
|
|
238
|
+
| `toolbar` | `ReactNode` | – | Your buttons at the end of the toolbar, such as a workflow status or Publish |
|
|
239
|
+
| `banner` | `ReactNode` | – | A strip under the toolbar |
|
|
240
|
+
| `tabs` | `{ id, label, content }[]` | `[]` | Extra tabs in the side panel, such as History |
|
|
241
|
+
| `notify` | `(msg: string) => void` | no-op | Success messages: connect your toast system here |
|
|
242
|
+
| `onError` | `(err: unknown) => void` | `console.error` | Error messages |
|
|
243
|
+
| `className` | `string` | `'h-full'` | Classes on the root element |
|
|
244
|
+
| `ref` | `Ref<FormEditorHandle>` | – | `{ save(), dirty, value }`, for example to save before a workflow step |
|
|
245
|
+
|
|
246
|
+
**Keyboard:** <kbd>Del</kbd> delete · <kbd>Ctrl</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Y</kbd> undo/redo ·
|
|
247
|
+
<kbd>Ctrl</kbd>+<kbd>D</kbd> duplicate · arrow keys nudge (hold <kbd>Shift</kbd> for larger steps) ·
|
|
248
|
+
<kbd>Esc</kbd> deselect
|
|
249
|
+
|
|
250
|
+
### `<FormFiller>`
|
|
251
|
+
|
|
252
|
+
| Prop | Type | Default | Description |
|
|
253
|
+
| --- | --- | --- | --- |
|
|
254
|
+
| `pdf` | `PdfSource` | **required** | The PDF |
|
|
255
|
+
| `fields` | `FieldDef[]` | **required** | The form's fields |
|
|
256
|
+
| `layout` | `Layout \| null` | `null` | The form's layout (`null` = the original pages) |
|
|
257
|
+
| `initialAnswers` | `Answers` | field defaults | A draft to continue |
|
|
258
|
+
| `onSave` | `(answers, { submit }) => unknown` | – | Called for drafts and autosave (`submit: false`) and for submit (`true`). Throw `{ errors }` to show field errors. |
|
|
259
|
+
| `onSubmitted` | `(answers) => void` | – | Called after a successful submit |
|
|
260
|
+
| `autosave` | `number \| false` | `2500` | Delay in ms after the last change |
|
|
261
|
+
| `onUpload` | `(field, file) => Promise<AnswerFileInfo>` | – | Stores an uploaded file or a signature PNG and returns its `id`. Without it, file fields are read-only and signatures are kept as data URLs. |
|
|
262
|
+
| `loadFile` | `(id) => Promise<Blob>` | – | Loads stored signatures and opens attachments |
|
|
263
|
+
| `files` | `AnswerFileInfo[]` | `[]` | Files already attached to the draft |
|
|
264
|
+
| `readOnly` | `boolean` | `false` | View a submitted form |
|
|
265
|
+
| `title`, `onBack`, `toolbar`, `notify`, `onError`, `className` | | | Same as `FormEditor` |
|
|
266
|
+
|
|
267
|
+
### Core functions (`formlift`)
|
|
268
|
+
|
|
269
|
+
| Function | Returns | Description |
|
|
270
|
+
| --- | --- | --- |
|
|
271
|
+
| `validateFields(fields, pageCount)` | `string[]` | Problems in a form definition: shapes, unique keys, positions, options, regexes, condition references and cycles |
|
|
272
|
+
| `validateLayout(layout, pageCount)` | `string[]` | Problems in a layout |
|
|
273
|
+
| `validateAnswers(fields, answers)` | `Record<key, message>` | Problems in the answers, with conditions applied |
|
|
274
|
+
| `cleanAnswers(fields, input)` | `Answers` | Keeps only known keys with the right value types |
|
|
275
|
+
| `checkAnswer(field, value, required)` | `string \| null` | Checks a single answer |
|
|
276
|
+
| `fieldStates(fields, answers)` | `Record<key, { visible, required, readOnly }>` | Applies the conditions |
|
|
277
|
+
| `evaluate(condition, answers)` | `boolean` | Evaluates one condition |
|
|
278
|
+
| `defaultAnswers(fields)` | `Answers` | The answers a new submission starts with |
|
|
279
|
+
| `defaultLayout(pageSizes)` | `Layout` | The layout of an untouched document |
|
|
280
|
+
| `detectDocument(pdf, OPS, pages, existing, onProgress?, signal?)` | `Promise<FieldDef[]>` | Suggests fields for a whole document |
|
|
281
|
+
| `toQuestions(fields, textOf?)` | `Question[]` | Fields grouped as questions, used by the list view |
|
|
282
|
+
|
|
283
|
+
### Server (`formlift/server`)
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
completedPdf(
|
|
287
|
+
original: Uint8Array | ArrayBuffer,
|
|
288
|
+
fields: FieldDef[],
|
|
289
|
+
answers: Answers,
|
|
290
|
+
files?: Record<string, { fileName: string; fileType: string; bytes: Uint8Array }>,
|
|
291
|
+
layout?: Layout | null,
|
|
292
|
+
): Promise<Uint8Array>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Data model
|
|
296
|
+
|
|
297
|
+
A form is plain, serialisable JSON:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
type FormDoc = { fields: FieldDef[]; layout: Layout | null }
|
|
301
|
+
|
|
302
|
+
interface FieldDef {
|
|
303
|
+
id: string
|
|
304
|
+
key: string // answer key: starts with a letter; letters, digits and _
|
|
305
|
+
type: 'text' | 'textarea' | 'number' | 'date' | 'email' | 'phone'
|
|
306
|
+
| 'dropdown' | 'radio' | 'checkbox' | 'file' | 'signature'
|
|
307
|
+
label: string
|
|
308
|
+
source: 'manual' | 'ai' // 'ai' = suggested by auto-detect
|
|
309
|
+
confidence?: number
|
|
310
|
+
position: { page: number; x: number; y: number; width: number; height: number } // 0..1 of the page, origin top-left
|
|
311
|
+
placeholder?: string
|
|
312
|
+
helpText?: string
|
|
313
|
+
defaultValue?: string
|
|
314
|
+
required?: boolean
|
|
315
|
+
readOnly?: boolean
|
|
316
|
+
hidden?: boolean
|
|
317
|
+
minLength?: number
|
|
318
|
+
maxLength?: number
|
|
319
|
+
min?: number
|
|
320
|
+
max?: number
|
|
321
|
+
pattern?: string
|
|
322
|
+
options?: string[] // dropdown, radio
|
|
323
|
+
visibleWhen?: Condition
|
|
324
|
+
requiredWhen?: Condition
|
|
325
|
+
readOnlyWhen?: Condition
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface Condition {
|
|
329
|
+
logic: 'and' | 'or'
|
|
330
|
+
rules: (Rule | Condition)[]
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
interface Rule {
|
|
334
|
+
fieldKey: string
|
|
335
|
+
op: 'equals' | 'notEquals' | 'contains' | 'startsWith' | 'endsWith'
|
|
336
|
+
| 'greaterThan' | 'lessThan' | 'between' | 'isEmpty' | 'isNotEmpty'
|
|
337
|
+
value?: string
|
|
338
|
+
value2?: string // upper bound for 'between'
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
type Answers = Record<string, string | boolean | null>
|
|
342
|
+
// checkbox → boolean · file / signature → your file id · everything else → string
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Positions are stored as page fractions, so a field never moves when the zoom or screen size changes.
|
|
346
|
+
|
|
347
|
+
## Recipes
|
|
348
|
+
|
|
349
|
+
<details>
|
|
350
|
+
<summary><b>Next.js (App Router)</b></summary>
|
|
351
|
+
|
|
352
|
+
The components use browser APIs, so render them on the client:
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
'use client'
|
|
356
|
+
import dynamic from 'next/dynamic'
|
|
357
|
+
import 'formlift/style.css'
|
|
358
|
+
|
|
359
|
+
const FormEditor = dynamic(() => import('formlift/react').then((m) => m.FormEditor), { ssr: false })
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
</details>
|
|
363
|
+
|
|
364
|
+
<details>
|
|
365
|
+
<summary><b>Loading a PDF that needs authentication</b></summary>
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
<FormFiller
|
|
369
|
+
pdf={() => fetch(`/api/forms/${id}/pdf`, { headers: { Authorization: `Bearer ${token}` } }).then((r) => r.blob())}
|
|
370
|
+
fields={form.fields}
|
|
371
|
+
/>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
</details>
|
|
375
|
+
|
|
376
|
+
<details>
|
|
377
|
+
<summary><b>Approval workflow (draft → review → publish)</b></summary>
|
|
378
|
+
|
|
379
|
+
formlift has no opinion about workflows. Add your own buttons and save first through the ref:
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
const editor = useRef<FormEditorHandle>(null)
|
|
383
|
+
|
|
384
|
+
<FormEditor
|
|
385
|
+
ref={editor}
|
|
386
|
+
pdf={pdf}
|
|
387
|
+
onSave={save}
|
|
388
|
+
banner={status === 'published' && <p className="notice">This version is live.</p>}
|
|
389
|
+
toolbar={
|
|
390
|
+
<button onClick={async () => (await editor.current!.save()) && sendForReview()}>
|
|
391
|
+
Send for review
|
|
392
|
+
</button>
|
|
393
|
+
}
|
|
394
|
+
tabs={[{ id: 'history', label: 'History', content: <History formId={id} /> }]}
|
|
395
|
+
/>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
</details>
|
|
399
|
+
|
|
400
|
+
<details>
|
|
401
|
+
<summary><b>Auto-detect without React (Node or any framework)</b></summary>
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import { getDocument, OPS } from 'pdfjs-dist/legacy/build/pdf.mjs'
|
|
405
|
+
import { detectDocument, type Ops } from 'formlift'
|
|
406
|
+
|
|
407
|
+
const pdf = await getDocument({ data }).promise
|
|
408
|
+
const pages = []
|
|
409
|
+
for (let i = 1; i <= pdf.numPages; i++) {
|
|
410
|
+
const [x0, y0, x1, y1] = (await pdf.getPage(i)).view
|
|
411
|
+
pages.push({ source: i, width: x1 - x0, height: y1 - y0 })
|
|
412
|
+
}
|
|
413
|
+
const fields = await detectDocument(pdf, OPS as unknown as Ops, pages, [])
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
</details>
|
|
417
|
+
|
|
418
|
+
## Theming
|
|
419
|
+
|
|
420
|
+
The styles live inside `.fl-root` and have no global reset. Override any variable to match your brand:
|
|
421
|
+
|
|
422
|
+
```css
|
|
423
|
+
.fl-root {
|
|
424
|
+
--fl-primary: #7c3aed;
|
|
425
|
+
--fl-primary-hover: #6d28d9;
|
|
426
|
+
--fl-primary-soft: #f5f3ff;
|
|
427
|
+
--fl-primary-tint: #ede9fe;
|
|
428
|
+
--fl-heading: #8b5cf6; /* panel headings */
|
|
429
|
+
--fl-accent: #f59e0b; /* suggested fields */
|
|
430
|
+
--fl-font: 'Inter', sans-serif;
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
The full list of variables is in [`src/style.css`](./src/style.css).
|
|
435
|
+
|
|
436
|
+
## Security
|
|
437
|
+
|
|
438
|
+
- Run `validateFields` / `validateLayout` when a form is saved, and `cleanAnswers` + `validateAnswers` when
|
|
439
|
+
answers are submitted. Browser-side checks are for user experience only.
|
|
440
|
+
- pdf.js runs with `isEvalSupported: false`, so code embedded in a document is never compiled.
|
|
441
|
+
- Patterns in fields (`pattern`) are compiled safely. An invalid regex is rejected, never thrown.
|
|
442
|
+
- formlift never uploads anything on its own. All network traffic goes through your callbacks.
|
|
443
|
+
|
|
444
|
+
## Limitations
|
|
445
|
+
|
|
446
|
+
- **Text-based PDFs.** Scanned (image-only) PDFs have no vector drawing to detect, and there is no OCR. Fields
|
|
447
|
+
can still be placed by hand.
|
|
448
|
+
- **Latin fonts in the completed PDF.** It uses the standard PDF fonts (WinAnsi), so other scripts print as `?`.
|
|
449
|
+
- **Rotated pages** (`/Rotate`) are not handled in the completed PDF.
|
|
450
|
+
- **PDF only.** Export Word or Excel files to PDF first.
|
|
451
|
+
|
|
452
|
+
## FAQ
|
|
453
|
+
|
|
454
|
+
**Does it use AI or send my documents anywhere?**
|
|
455
|
+
No. Detection is geometric and runs locally in the browser (or in Node). Nothing leaves your app unless your
|
|
456
|
+
own callbacks send it.
|
|
457
|
+
|
|
458
|
+
**Can I use it without React?**
|
|
459
|
+
Yes. `formlift` and `formlift/server` have no UI. The React package is one way to render the form, and
|
|
460
|
+
`FieldDef` positions make it straightforward to render with any framework.
|
|
461
|
+
|
|
462
|
+
**Where are forms stored?**
|
|
463
|
+
Wherever you want. A form is `{ fields, layout }` JSON and answers are a flat `{ key: value }` object.
|
|
464
|
+
|
|
465
|
+
**Will the styles conflict with Tailwind or my design system?**
|
|
466
|
+
No. Every rule and CSS variable is scoped to `.fl-root`.
|
|
467
|
+
|
|
468
|
+
## Contributing
|
|
469
|
+
|
|
470
|
+
Contributions are welcome: bug reports, fixes, new auto-detect cases and docs. Read
|
|
471
|
+
[CONTRIBUTING.md](./CONTRIBUTING.md) for the setup and guidelines. In short:
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
git clone https://github.com/<your-username>/formlift.git && cd formlift
|
|
475
|
+
npm install # installs and builds
|
|
476
|
+
npm run dev # live demo at http://localhost:5173
|
|
477
|
+
npm test # must pass before a pull request
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## License
|
|
481
|
+
|
|
482
|
+
[MIT](./LICENSE) © Harendra Singh Negi
|