Skip to content

Commit 980906d

Browse files
authored
feat(P074): consolidate React embed on the @simplepdf/embed core (#38)
## Background Phase 7 of P074 (`plans/P074-embed-bridge-refactor.md`): consolidate `@simplepdf/react-embed-pdf` onto the framework-free `@simplepdf/embed` core so both packages share one generated, camelCase, agentic surface — shipped as a non-breaking minor (`@simplepdf/embed@0.5.0`, `@simplepdf/react-embed-pdf@1.11.0`). ## Changes - **embed — grouped handle**: `createEmbed` returns `{ actions, events, lifecycle }` — `embed.actions.*` (operations), `embed.events.on(type, handler)` (granular subscriptions), `embed.lifecycle.dispose()`. - **embed — camelCase SDK**: methods, args, results, and agentic tool names are camelCase, generated from `embed-api.json`; the snake_case wire is transformed at the `postMessage` boundary. Outbound events stay verbatim snake_case (the stable 1.x contract). - **embed — misc**: `companyIdentifier` replaces `tenant`; SimplePDF documents URLs load directly; `routeToolCall` takes the actions group; `zod` is an opt-in peer (`/tools`, `/ai-sdk`, `/schemas`); the main entry is zero-dependency. - **react — non-breaking rebuild (`1.11.0`)**: built on the core; the flat 1.x `EmbedActions` ref is preserved (`embedRef.current.selectTool(...)`), `onEmbedEvent` stays verbatim, modal is still the default, `documentURL` is a deprecated alias for `document`. `useEmbed().actions` is now the full editor surface; `selectTool`/`submit` keep their 1.x argument overloads. - **react — agentic opt-in**: the tools moved to a new `@simplepdf/react-embed-pdf/ai-sdk` subpath (`useEmbedTools(embedRef)`), so the package root stays zod-free — mirroring the core's main vs `/ai-sdk` split. - **copilot**: dog-foods the new API (`useEmbedTools`, camelCase tools/actions, `DOCUMENT_LOADED` readiness). - **docs**: the `embed` + `react` READMEs and `documentation/IFRAME.md` are rewritten for the grouped handle + agentic ("fill and read this for me") examples. ## Notes - Versions bump via the Changeset Release flow: merge this PR → a **"Version Packages"** PR (`embed@0.5.0`, `react@1.11.0`) → merge that → auto-publish to npm (`latest`). Both changesets are `minor` (no major). - `copilot/package-lock.json` is regenerated **after** the packages publish (it cannot reference versions that are not yet on npm): publish → `npm install` in `copilot/` → deploy copilot (manual; `deploy_on_push: false`).
1 parent ff86476 commit 980906d

84 files changed

Lines changed: 2469 additions & 5653 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@simplepdf/embed": minor
3+
---
4+
5+
camelCase SDK surface grouped into `actions` / `events` / `lifecycle`, `companyIdentifier`, and direct loading of SimplePDF documents URLs.
6+
7+
- **Grouped handle**: `createEmbed` returns `{ actions, events, lifecycle }``embed.actions.*` (operations), `embed.events.on(type, handler)` (subscriptions), `embed.lifecycle.dispose()` (teardown).
8+
- **camelCase everywhere on the SDK**, with the snake_case wire kept behind a transform owned by the bridge: method names + their arguments + results + the agentic tool names/args are camelCase (`embed.actions.getFields()`, `embed.actions.setFieldValue({ fieldId, value })`, `embed.actions.submit({ downloadCopy })`, `tools.getDocumentContent`). The editor's snake_case wire is generated from `embed-api.json` and transformed at the postMessage boundary — consumers never see it.
9+
- **Events are the deliberate exception**: `embed.events.on(type, handler)` delivers the editor's outbound payloads VERBATIM (snake_case fields, e.g. `document_id`) for `EDITOR_READY` / `DOCUMENT_LOADED` / `PAGE_FOCUSED` / `SUBMISSION_SENT`, so the React layer's `onEmbedEvent` is unchanged.
10+
- **`companyIdentifier`** replaces `tenant` in `createEmbed` (it is the consumer's own SimplePDF subdomain — `tenant` read as if SimplePDF were multi-tenant per consumer).
11+
- **Documents URLs load directly**: when `document.url` is a `<tenant>.<baseDomain>/documents/<id>` URL (https, single tenant label), `createEmbed` navigates the iframe straight to it (carrying `?context=`) instead of host-fetching — so prefilled/stored documents open as themselves.
12+
- The React layer moved OUT of this package into `@simplepdf/react-embed-pdf` (the `/react` subpath is removed); the editor iframe is granted `clipboard-read; clipboard-write` by default.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@simplepdf/react-embed-pdf": minor
3+
---
4+
5+
Rebuilt on the `@simplepdf/embed` core, adding an AI-SDK-native agentic surface — a non-breaking superset of the existing component API.
6+
7+
`@simplepdf/react-embed-pdf` no longer hand-rolls its own iframe bridge; it is a thin React layer over the shared `@simplepdf/embed` core (the same core `web-embed-pdf` and future framework adapters sit on).
8+
9+
**The existing `<EmbedPDF>` contract is preserved (drop-in):** the props (`companyIdentifier`, `documentURL`, `mode` — still defaulting to `"modal"`, `onEmbedEvent`, `locale`, `baseDomain`, `context`, `className`, `style`) and, crucially, `onEmbedEvent` still emits the editor's events VERBATIM: `{ type: 'EDITOR_READY' | 'DOCUMENT_LOADED' | 'PAGE_FOCUSED' | 'SUBMISSION_SENT', data }` with snake_case payloads. `useEmbed()` still returns `{ embedRef, actions }`.
10+
11+
**New (additive):**
12+
13+
- A new opt-in `@simplepdf/react-embed-pdf/ai-sdk` subpath exposes the agentic surface: `useEmbedTools(embedRef)` binds the tool registry to the live editor for the Vercel AI SDK (`useChat({ tools })`), plus `simplePDFToolDefinitions` (server) and `createSimplePDFExecutor`. It mirrors `@simplepdf/embed`'s `/ai-sdk`, so the package root stays zod-free.
14+
- `useEmbed().actions` now exposes the FULL editor surface (camelCase): `createField`, `getFields`, `setFieldValue`, `focusField`, `movePage`, `rotatePage`, `deletePages`, `download`, … — not just the original six.
15+
- A typed `document` prop (`{ url } | { dataUrl } | { file }`), the same shape as `createEmbed`. It also accepts data URLs and File/Blob, and a SimplePDF documents URL loads directly (prefill etc.). `documentURL` is now `@deprecated` (still works) in favor of it.
16+
- An optional `logger` prop surfaces the bridge's structured lifecycle/error logging.
17+
- The forwarded `ref` (`embedRef.current`) stays the flat actions handle — `embedRef.current.selectTool(...)`, etc. — now exposing the full camelCase action set. (The framework-free `@simplepdf/embed` core groups its handle as `embed.actions` / `embed.events` / `embed.lifecycle`; the React layer flattens it to keep the existing ref contract.)
18+
19+
**Imperative actions stay backward-compatible.** `selectTool` and `submit` gained camelCase argument shapes to match the rest of the SDK (`selectTool({ tool })`, `submit({ downloadCopy })`), but the previous forms — `selectTool(toolType)` and `submit({ downloadCopyOnDevice })` — still work as deprecated overloads that normalize to the new shape, so existing `useEmbed().actions` callers don't change. A relative `documentURL` / trigger `href` (e.g. `/form.pdf`) is still accepted — it is resolved against the page URL, as before.
20+
21+
One behavioral note: calling an action before `<EmbedPDF>` has mounted now resolves to `{ success: false, error: { code: 'unexpected:iframe_not_mounted' } }` (the previous form returned `bad_request:embed_ref_not_available`). Code that checks `result.success` is unaffected; only code branching on the exact pre-mount error string needs updating.
22+
23+
Packaging is preserved: still dual CJS + ESM, so `require()` consumers keep working. `zod` remains a peer dependency, now required **only** by the agentic `/ai-sdk` subpath (it validates tool input) — the package root (`<EmbedPDF>`, `useEmbed`) is zod-free, so a non-agentic app never loads it. Install `zod` only if you import `/ai-sdk`; npm 7+ adds it automatically, pnpm / Yarn PnP users add it explicitly.

.changeset/rename-boxed-text-to-comb-text.md

Lines changed: 0 additions & 15 deletions
This file was deleted.

.changeset/rename-remove-fields-to-delete-fields.md

Lines changed: 0 additions & 21 deletions
This file was deleted.

.changeset/replace-create-field-with-detect-fields.md

Lines changed: 0 additions & 15 deletions
This file was deleted.

.github/workflows/embed.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Embed
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- embed/**
9+
- .github/workflows/embed.yaml
10+
pull_request:
11+
branches:
12+
- main
13+
paths:
14+
- embed/**
15+
- .github/workflows/embed.yaml
16+
17+
jobs:
18+
test:
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Checkout code
23+
uses: actions/checkout@v4
24+
25+
- name: Set up Node.js
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: "22"
29+
30+
# @simplepdf/embed is a workspace package, so install from the root.
31+
- name: Install dependencies
32+
run: npm ci
33+
34+
- name: Types
35+
run: npm run --workspace @simplepdf/embed test:types
36+
37+
- name: Tests
38+
run: npm run --workspace @simplepdf/embed test
39+
40+
- name: Bundle size budgets
41+
run: npm run --workspace @simplepdf/embed check:size

.github/workflows/react.yaml

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,19 @@ on:
66
- main
77
paths:
88
- react/**
9+
- embed/**
910
- .github/workflows/react.yaml
1011
pull_request:
1112
branches:
1213
- main
1314
paths:
1415
- react/**
16+
- embed/**
1517
- .github/workflows/react.yaml
1618

1719
jobs:
1820
test:
1921
runs-on: ubuntu-latest
20-
defaults:
21-
run:
22-
working-directory: react
2322

2423
steps:
2524
- name: Checkout code
@@ -30,14 +29,20 @@ jobs:
3029
with:
3130
node-version: "22"
3231

32+
# react-embed-pdf depends on the @simplepdf/embed workspace package, so install
33+
# from the workspace ROOT (the internal dep resolves via the workspace, not npm)
34+
# rather than a per-package `npm ci`, and build the core before type-checking.
3335
- name: Install dependencies
3436
run: npm ci
3537

38+
- name: Build the core
39+
run: npm run --workspace @simplepdf/embed build
40+
3641
- name: Formatting
37-
run: npm run test:format
42+
run: npm run --workspace @simplepdf/react-embed-pdf test:format
3843

3944
- name: Types
40-
run: npm run test:types
45+
run: npm run --workspace @simplepdf/react-embed-pdf test:types
4146

4247
- name: Tests
43-
run: npm test
48+
run: npm run --workspace @simplepdf/react-embed-pdf test

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ https://github.com/SimplePDF/simplepdf-embed/assets/10613140/8924f018-6076-4e44-
3939

4040
# Get started
4141

42-
- 🧩 [Typed bridge](./embed/README.md) - `@simplepdf/embed` (typed client + React hook + AI SDK adapter, generated from the editor manifest)
42+
- 🧩 [Iframe bridge](./embed/README.md) - `@simplepdf/embed` (framework-free client to embed + programmatically drive the editor, with an AI SDK adapter, generated from the editor manifest; the React layer is `@simplepdf/react-embed-pdf`)
4343
- ⚛️ [React component](./react/README.md) - `@simplepdf/react-embed-pdf`
4444
- 🚀 [Script tag](./web/README.md) - `@simplepdf/web-embed-pdf`
4545
- 🛠 [Iframe API](./documentation/IFRAME.md) - `postMessage` events
@@ -146,7 +146,7 @@ With a [Pro plan](https://simplepdf.com/pricing), you can:
146146

147147
```jsx
148148
// React - branding configured in your dashboard settings
149-
<EmbedPDF companyIdentifier="yourcompany">
149+
<EmbedPDF mode="modal" companyIdentifier="yourcompany">
150150
<button>Edit PDF</button>
151151
</EmbedPDF>
152152
```
@@ -252,16 +252,16 @@ Use `getDocumentContent()` to extract text from the PDF. See the [React](./react
252252

253253
### Downloading the modified PDF
254254

255-
Use `submit({ downloadCopyOnDevice: true })` to trigger a browser download of the modified PDF.
255+
Use `submit({ downloadCopy: true })` to trigger a browser download of the modified PDF.
256256

257257
### Server-side PDF generation & storage
258258

259259
SimplePDF handles PDF generation and storage so you don't have to. When users submit, the filled PDF is automatically generated and stored - either on SimplePDF's servers or your own storage.
260260

261261
| Method | How it works | Use case |
262262
| ------------------------------------------- | ----------------------------- | ------------------------------------ |
263-
| `submit` with `downloadCopyOnDevice: true` | Browser downloads the PDF | End-user saves their work |
264-
| `submit` with `downloadCopyOnDevice: false` | PDF sent to SimplePDF servers | Server-side collection via webhooks |
263+
| `submit` with `downloadCopy: true` | Browser downloads the PDF | End-user saves their work |
264+
| `submit` with `downloadCopy: false` | PDF sent to SimplePDF servers | Server-side collection via webhooks |
265265
| S3/Azure/SharePoint integration | PDF stored in your storage | Programmatic access via your storage |
266266

267267
**Available integrations:**
@@ -349,7 +349,7 @@ Yes. Use viewer mode to display PDFs without any editing capabilities.
349349
<EmbedPDF
350350
companyIdentifier="react-viewer"
351351
mode="inline"
352-
documentURL="https://example.com/document.pdf"
352+
document={{ url: 'https://example.com/document.pdf' }}
353353
style={{ width: 900, height: 800 }}
354354
/>
355355
```

copilot/README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ See the privacy notes above for the per-route audio-egress disclosure.
144144

145145
### Load a specific document via `?url=`
146146

147-
To open a specific document instead of the bundled demo forms, append `?url=<document-url>`. The value is used verbatim as the editor iframe `src`, so pass a valid SimplePDF document URL (e.g. a `/documents/<id>` link, optionally with a `?prefill=<id>`):
147+
To open a specific document instead of the bundled demo forms, append `?url=<document-url>`. The value is passed as the `document` to `<EmbedPDF>`; a SimplePDF documents URL (a `/documents/<id>` link, optionally with a `?prefill=<id>`) is navigated to directly by the embed core, so pass a valid one:
148148

149149
```
150150
http://localhost:3001/?url=https%3A%2F%2Fdemo.simplepdf.com%2Fdocuments%2Fc28f061b-1974-4251-ba7a-d08bedc3ef28%3Fprefill%3D35fdf39e-2e06-4712-bb9d-f62d2f88ce50
@@ -212,16 +212,16 @@ The chat sidebar advertises these tools to the model. Each runs inside the ifram
212212

213213
| Tool | Purpose |
214214
|------|---------|
215-
| `get_fields` | List form fields currently on the document |
216-
| `get_document_content` | Extract text content per page |
217-
| `detect_fields` | Auto-detect missing fields on scanned PDFs |
218-
| `focus_field` | Highlight + scroll to a field |
219-
| `set_field_value` | Write a value into a field |
220-
| `select_tool` | Switch the editor toolbar (`TEXT`, `COMB_TEXT`, `CHECKBOX`, `SIGNATURE`, `PICTURE`) |
221-
| `go_to` | Navigate to a specific page (1-indexed) |
222-
| `move_page` | Reorder a visible page (`from_page``to_page`, both 1-indexed). Destructive — only fired on explicit user request |
223-
| `delete_page` | Remove a visible page and its fields (last remaining page can't be deleted). Destructive — only fired on explicit user request |
224-
| `rotate_page` | Rotate a visible page 90° clockwise per call. Destructive — only fired on explicit user request |
215+
| `getFields` | List form fields currently on the document |
216+
| `getDocumentContent` | Extract text content per page |
217+
| `detectFields` | Auto-detect missing fields on scanned PDFs |
218+
| `focusField` | Highlight + scroll to a field |
219+
| `setFieldValue` | Write a value into a field |
220+
| `selectTool` | Switch the editor toolbar (`TEXT`, `COMB_TEXT`, `CHECKBOX`, `SIGNATURE`, `PICTURE`) |
221+
| `goTo` | Navigate to a specific page (1-indexed) |
222+
| `movePage` | Reorder a visible page (`fromPage``toPage`, both 1-indexed). Destructive — only fired on explicit user request |
223+
| `deletePages` | Remove visible pages and their fields (last remaining page can't be deleted). Destructive — only fired on explicit user request |
224+
| `rotatePage` | Rotate a visible page 90° clockwise per call. Destructive — only fired on explicit user request |
225225
| `submit` (Pro mode) / `download` (demo mode) | Finalize: real iframe `SUBMIT` on a Pro fork (lands in BYOS + webhooks) vs. an in-browser `DOWNLOAD` on the hosted demo |
226226

227227
Tool input + output schemas + the bridge that posts these events into the iframe live in the [`@simplepdf/embed`](../embed) package (generated from the editor contract); copilot's tool catalogue + middleware live in `src/lib/tools/` (`definitions.ts`, `middleware.ts`). System prompt: `src/server/tools.ts`. Public iframe contract these tools exercise: [`documentation/IFRAME.md`](../documentation/IFRAME.md).

copilot/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"@ai-sdk/openai": "^3.0.53",
2828
"@ai-sdk/react": "^3.0.170",
2929
"@aws-sdk/client-s3": "^3.1034.0",
30-
"@simplepdf/embed": "^0.4.0",
30+
"@simplepdf/embed": "0.5.0",
31+
"@simplepdf/react-embed-pdf": "1.11.0",
3132
"@tailwindcss/vite": "^4.1.18",
3233
"@tanstack/react-devtools": "latest",
3334
"@tanstack/react-router": "latest",

0 commit comments

Comments
 (0)