diff --git a/.changeset/rename-boxed-text-to-comb-text.md b/.changeset/rename-boxed-text-to-comb-text.md new file mode 100644 index 0000000..5123708 --- /dev/null +++ b/.changeset/rename-boxed-text-to-comb-text.md @@ -0,0 +1,15 @@ +--- +"@simplepdf/react-embed-pdf": major +--- + +Renames the `BOXED_TEXT` tool type to `COMB_TEXT`. "Comb" is the Acrobat / PDF-spec term for box-per-character fields (IBAN, dates, CERFA), so `actions.selectTool(...)` and the `SELECT_TOOL` iframe event now use `COMB_TEXT`, and the `ToolType` union exposes `COMB_TEXT` instead of `BOXED_TEXT`. + +Already-deployed embeds keep working without a code change: the editor still accepts the legacy `BOXED_TEXT` value at runtime, so this only changes the TypeScript type. If you are not calling `actions.selectTool('BOXED_TEXT')` or `sendEvent("SELECT_TOOL", { tool: "BOXED_TEXT" })`, you can safely update to this new major version. + +```ts +// Before +await actions.selectTool('BOXED_TEXT'); + +// After +await actions.selectTool('COMB_TEXT'); +``` diff --git a/copilot/README.md b/copilot/README.md index 9e98bb2..10ba844 100644 --- a/copilot/README.md +++ b/copilot/README.md @@ -193,7 +193,7 @@ The chat sidebar advertises these tools to the model. Each runs inside the ifram | `detect_fields` | Auto-detect missing fields on scanned PDFs | | `focus_field` | Highlight + scroll to a field | | `set_field_value` | Write a value into a field | -| `select_tool` | Switch the editor toolbar (`TEXT`, `BOXED_TEXT`, `CHECKBOX`, `SIGNATURE`, `PICTURE`) | +| `select_tool` | Switch the editor toolbar (`TEXT`, `COMB_TEXT`, `CHECKBOX`, `SIGNATURE`, `PICTURE`) | | `go_to_page` | Navigate to a specific page (1-indexed) | | `move_page` | Reorder a visible page (`from_page` → `to_page`, both 1-indexed). Destructive — only fired on explicit user request | | `delete_page` | Remove a visible page and its fields (last remaining page can't be deleted). Destructive — only fired on explicit user request | diff --git a/copilot/src/components/chat/chat_pane.tsx b/copilot/src/components/chat/chat_pane.tsx index dcbc331..22d97aa 100644 --- a/copilot/src/components/chat/chat_pane.tsx +++ b/copilot/src/components/chat/chat_pane.tsx @@ -138,7 +138,7 @@ type CompactedDocumentContent = { name: string | null; pages: DocumentContentPag const isToolbarTool = (value: unknown): value is ToolbarTool => value === null || value === 'TEXT' || - value === 'BOXED_TEXT' || + value === 'COMB_TEXT' || value === 'CHECKBOX' || value === 'SIGNATURE' || value === 'PICTURE' @@ -156,7 +156,7 @@ type PlacementTool = Exclude // dedupes for icon rendering. type NewFieldHintMetadata = { kind: 'new_field_hint'; tools: PlacementTool[]; delta: number } -const PLACEMENT_TOOLS: readonly PlacementTool[] = ['TEXT', 'BOXED_TEXT', 'CHECKBOX', 'SIGNATURE', 'PICTURE'] +const PLACEMENT_TOOLS: readonly PlacementTool[] = ['TEXT', 'COMB_TEXT', 'CHECKBOX', 'SIGNATURE', 'PICTURE'] const isPlacementTool = (value: unknown): value is PlacementTool => typeof value === 'string' && PLACEMENT_TOOLS.some((candidate) => candidate === value) @@ -174,7 +174,9 @@ const buildNewFieldMessage = ({ if (uniqueTools.length === 1) { return `${delta} new ${uniqueTools[0]} fields were just added to the document. Please continue helping me with them.` } - const breakdown = uniqueTools.map((tool) => `${tools.filter((t) => t === tool).length} ${tool}`).join(', ') + const breakdown = uniqueTools + .map((tool) => `${tools.filter((t) => t === tool).length} ${tool}`) + .join(', ') return `${delta} new fields were just added to the document (${breakdown}). Please continue helping me with them.` })() return { text, metadata: { kind: 'new_field_hint', tools, delta } } @@ -859,6 +861,7 @@ export const ChatPane = ({ // the visible width. Reset height to 'auto' first so scrollHeight reflects // the wrapped content, then clamp to MAX. The CSS transition on the element // animates the height change. + // biome-ignore lint/correctness/useExhaustiveDependencies: `draft` is a deliberate re-run trigger (the body reads scrollHeight, not draft) so the textarea resizes on every keystroke; removing it freezes the height. useLayoutEffect(() => { const textarea = inputRef.current if (textarea === null) { diff --git a/copilot/src/components/chat/toolbar.tsx b/copilot/src/components/chat/toolbar.tsx index a76f2ff..24c6f85 100644 --- a/copilot/src/components/chat/toolbar.tsx +++ b/copilot/src/components/chat/toolbar.tsx @@ -19,7 +19,7 @@ type ToolbarProps = { onFinalisation: () => void } -const BoxedTextIcon = ({ size = 14 }: { size?: number; strokeWidth?: number }) => ( +const CombTextIcon = ({ size = 14 }: { size?: number; strokeWidth?: number }) => ( (inputSchema: TSchema): { description: string; inputSchema: TSchema } => ({ +const tool = ( + inputSchema: TSchema, +): { description: string; inputSchema: TSchema } => ({ description: inputSchema.description ?? '', inputSchema, }) diff --git a/copilot/src/lib/embed-bridge/bridge.ts b/copilot/src/lib/embed-bridge/bridge.ts index a471cc8..5011bef 100644 --- a/copilot/src/lib/embed-bridge/bridge.ts +++ b/copilot/src/lib/embed-bridge/bridge.ts @@ -122,10 +122,7 @@ export const createBridge = ({ notify() } - const sendRequest = ( - type: BridgeRequestType, - data: unknown, - ): Promise> => + const sendRequest = (type: BridgeRequestType, data: unknown): Promise> => new Promise((resolve) => { const iframe = getIframe() if (iframe === null || iframe.contentWindow === null) { @@ -407,20 +404,23 @@ export const createBridge = ({ getState: () => state, loadDocument: (args) => parseAndSend(LoadDocumentInput, 'LOAD_DOCUMENT', args), getFields: () => sendRequest<{ fields: FieldRecord[] }>('GET_FIELDS', {}), - getDocumentContent: (args) => parseAndSend( - GetDocumentContentInput, - 'GET_DOCUMENT_CONTENT', - args, - ), + getDocumentContent: (args) => + parseAndSend( + GetDocumentContentInput, + 'GET_DOCUMENT_CONTENT', + args, + ), detectFields: () => sendRequest('DETECT_FIELDS', {}), - deleteFields: (args) => parseAndSend( - DeleteFieldsInput, - 'DELETE_FIELDS', - args, - ), + deleteFields: (args) => + parseAndSend( + DeleteFieldsInput, + 'DELETE_FIELDS', + args, + ), selectTool: (args) => parseAndSend(SelectToolInput, 'SELECT_TOOL', args), setFieldValue: (args) => parseAndSend(SetFieldValueInput, 'SET_FIELD_VALUE', args), - focusField: (args) => parseAndSend(FocusFieldInput, 'FOCUS_FIELD', args), + focusField: (args) => + parseAndSend(FocusFieldInput, 'FOCUS_FIELD', args), goTo: (args) => parseAndSend(GoToInput, 'GO_TO', args), movePage: (args) => parseAndSend(MovePageInput, 'MOVE_PAGE', args), deletePages: (args) => parseAndSend(DeletePagesInput, 'DELETE_PAGES', args), diff --git a/copilot/src/lib/embed-bridge/schemas.ts b/copilot/src/lib/embed-bridge/schemas.ts index a05b99d..f7ba0b0 100644 --- a/copilot/src/lib/embed-bridge/schemas.ts +++ b/copilot/src/lib/embed-bridge/schemas.ts @@ -10,7 +10,7 @@ import { z } from 'zod' // One file, one schema per operation. The shape is the snake_case payload // that travels over postMessage; nothing converts keys between layers. -export const SupportedFieldTypeSchema = z.enum(['TEXT', 'BOXED_TEXT', 'CHECKBOX', 'PICTURE', 'SIGNATURE']) +export const SupportedFieldTypeSchema = z.enum(['TEXT', 'COMB_TEXT', 'CHECKBOX', 'PICTURE', 'SIGNATURE']) export const NoInput = z.object({}) @@ -28,8 +28,16 @@ export const DetectFieldsInput = NoInput.describe( export const DeleteFieldsInput = z .object({ - field_ids: z.array(z.string()).optional().describe('Specific field identifiers to delete (omit to target by page or all)'), - page: z.number().int().positive().optional().describe('1-indexed visible page to clear (omit to target specific ids or all)'), + field_ids: z + .array(z.string()) + .optional() + .describe('Specific field identifiers to delete (omit to target by page or all)'), + page: z + .number() + .int() + .positive() + .optional() + .describe('1-indexed visible page to clear (omit to target specific ids or all)'), }) .describe( 'Deletes fields from the document. Pass field_ids to delete specific fields, page to clear a single page, or both omitted to delete every field. Destructive: only call when the user explicitly asks.', @@ -37,10 +45,12 @@ export const DeleteFieldsInput = z export const SelectToolInput = z .object({ - tool: SupportedFieldTypeSchema.nullable().describe('Editor tool to activate. Pass null to return to the cursor.'), + tool: SupportedFieldTypeSchema.nullable().describe( + 'Editor tool to activate. Pass null to return to the cursor.', + ), }) .describe( - 'Switches the active editor tool. Use tool="TEXT" for free-form text, "BOXED_TEXT" for box-per-character fields (e.g. IBAN), or any of the other field types to let the user drop fields on a document without native AcroFields.', + 'Switches the active editor tool. Use tool="TEXT" for free-form text, "COMB_TEXT" for box-per-character fields (e.g. IBAN), or any of the other field types to let the user drop fields on a document without native AcroFields.', ) export const SetFieldValueInput = z @@ -50,7 +60,7 @@ export const SetFieldValueInput = z .string() .nullable() .describe( - 'Value to write. TEXT/BOXED_TEXT: any string. CHECKBOX: "checked" ticks, null un-ticks (never "true"/"false"). Do not use this tool for SIGNATURE or PICTURE fields.', + 'Value to write. TEXT/COMB_TEXT: any string. CHECKBOX: "checked" ticks, null un-ticks (never "true"/"false"). Do not use this tool for SIGNATURE or PICTURE fields.', ), }) .describe('Writes a value into a single field in the PDF') diff --git a/copilot/src/lib/embed-bridge/types.ts b/copilot/src/lib/embed-bridge/types.ts index cd3b96c..bb71ef6 100644 --- a/copilot/src/lib/embed-bridge/types.ts +++ b/copilot/src/lib/embed-bridge/types.ts @@ -7,7 +7,12 @@ // in the union preserves IDE autocomplete for the bridge-owned literals // while still accepting arbitrary forwarded codes — narrowing on a // specific iframe code stays the consumer's responsibility. -type BridgeOwnedErrorCode = 'bad_input' | 'bridge_disposed' | 'iframe_not_ready' | 'missing_result' | 'timeout' +type BridgeOwnedErrorCode = + | 'bad_input' + | 'bridge_disposed' + | 'iframe_not_ready' + | 'missing_result' + | 'timeout' export type BridgeErrorCode = BridgeOwnedErrorCode | (string & {}) @@ -52,7 +57,7 @@ export const isBridgeResultLike = (value: unknown): value is BridgeResult { return } const secureFlag = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '' + // biome-ignore lint/suspicious/noDocumentCookie: deliberate first-party write; the CookieStore API is async and lacks the browser support this synchronous helper needs. document.cookie = `${WELCOME_DISMISSED_COOKIE}=1; path=/; max-age=31536000; SameSite=Lax${secureFlag}` } diff --git a/copilot/src/server/tools.ts b/copilot/src/server/tools.ts index 7d22719..e9901e5 100644 --- a/copilot/src/server/tools.ts +++ b/copilot/src/server/tools.ts @@ -109,7 +109,7 @@ Filling loop (ALWAYS keep going — do not hand control back until you genuinely - For SIGNATURE and PICTURE fields, call focus_field then stop — the user must sign / drop a picture themselves. Do not call set_field_value for these. - If the user has clearly indicated they want to type the value themselves (see Hesitancy handling), call focus_field then stop and wait. - Field value formats: - - TEXT / BOXED_TEXT: any string. + - TEXT / COMB_TEXT: any string. - CHECKBOX: value="checked" to tick, value=null to un-tick. NEVER use "true", "false", "yes", "no" for checkboxes — the editor will reject them. - SIGNATURE / PICTURE: do not call set_field_value. Use focus_field and hand off to the user. - After a successful set_field_value, IMMEDIATELY move to the next field — either set_field_value on it (if you already have the value) or ask exactly one question for that field. Do not send a standalone message like "Done" or "Now I'll move on". diff --git a/documentation/IFRAME.md b/documentation/IFRAME.md index 8dcb6ce..4e26af7 100644 --- a/documentation/IFRAME.md +++ b/documentation/IFRAME.md @@ -183,7 +183,7 @@ await sendEvent("LOAD_DOCUMENT", { await sendEvent("GO_TO", { page: 3 }); // Select a tool -await sendEvent("SELECT_TOOL", { tool: "TEXT" }); // or "CHECKBOX", "SIGNATURE", "PICTURE", "BOXED_TEXT", null +await sendEvent("SELECT_TOOL", { tool: "TEXT" }); // or "CHECKBOX", "SIGNATURE", "PICTURE", "COMB_TEXT", null // Detect fields in the document await sendEvent("DETECT_FIELDS", {}); @@ -290,7 +290,7 @@ Select a drawing tool or return to cursor mode. | Field | Type | Required | Description | | ------ | ---------------- | -------- | ---------------------------------------------------------------------------------------- | -| `tool` | `string \| null` | Yes | `"TEXT"`, `"BOXED_TEXT"`, `"CHECKBOX"`, `"SIGNATURE"`, `"PICTURE"`, or `null` for cursor | +| `tool` | `string \| null` | Yes | `"TEXT"`, `"COMB_TEXT"`, `"CHECKBOX"`, `"SIGNATURE"`, `"PICTURE"`, or `null` for cursor | #### DETECT_FIELDS diff --git a/examples/with-custom-sidebar/app/page.tsx b/examples/with-custom-sidebar/app/page.tsx index 3233c0a..d5589b6 100644 --- a/examples/with-custom-sidebar/app/page.tsx +++ b/examples/with-custom-sidebar/app/page.tsx @@ -5,7 +5,7 @@ import { Type, ImageIcon, MousePointer, Check, PenTool, FileText, Loader2 } from import { EmbedPDF, useEmbed } from "@simplepdf/react-embed-pdf" import { Switch } from "@/components/ui/switch" -type ToolType = 'TEXT' | 'BOXED_TEXT' | 'CHECKBOX' | 'PICTURE' | 'SIGNATURE' | null; +type ToolType = 'TEXT' | 'COMB_TEXT' | 'CHECKBOX' | 'PICTURE' | 'SIGNATURE' | null; export default function PDFEditorUI() { const [selectedTool, setSelectedTool] = useState(null) diff --git a/react/README.md b/react/README.md index 1e2b5d7..0412afc 100644 --- a/react/README.md +++ b/react/README.md @@ -137,14 +137,14 @@ _Some actions require a SimplePDF account. See [Retrieving PDF Data](../README.m Use `const { embedRef, actions } = useEmbed();` to programmatically control the embed editor: -| Action | Description | -| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -| `actions.goTo({ page })` | Navigate to a specific page | -| `actions.selectTool(toolType)` | Select a tool: `'TEXT'`, `'BOXED_TEXT'`, `'CHECKBOX'`, `'PICTURE'`, `'SIGNATURE'`, or `null` to deselect (`CURSOR`) | -| `actions.detectFields()` | Automatically detect form fields in the document | -| `actions.deleteFields(options?)` | Delete fields by `fieldIds` or `page`, or all fields if no options | -| `actions.getDocumentContent({ extractionMode })` | Extract document content (`extractionMode: 'auto'` or `'ocr'`) | -| `actions.submit({ downloadCopyOnDevice })` | Submit the document | +| Action | Description | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | +| `actions.goTo({ page })` | Navigate to a specific page | +| `actions.selectTool(toolType)` | Select a tool: `'TEXT'`, `'COMB_TEXT'`, `'CHECKBOX'`, `'PICTURE'`, `'SIGNATURE'`, or `null` to deselect (`CURSOR`) | +| `actions.detectFields()` | Automatically detect form fields in the document | +| `actions.deleteFields(options?)` | Delete fields by `fieldIds` or `page`, or all fields if no options | +| `actions.getDocumentContent({ extractionMode })` | Extract document content (`extractionMode: 'auto'` or `'ocr'`) | +| `actions.submit({ downloadCopyOnDevice })` | Submit the document | All actions return a `Promise` with a result object: `{ success: true, data: ... }` or `{ success: false, error: { code, message } }`. diff --git a/react/src/hook.test.ts b/react/src/hook.test.ts index ec43ef8..bce1572 100644 --- a/react/src/hook.test.ts +++ b/react/src/hook.test.ts @@ -374,7 +374,7 @@ describe('Type assertions', () => { // These types are intentionally inlined to act as a "frozen" contract. // If the actual types change, these tests will fail at compile time. - type ExpectedToolType = 'TEXT' | 'BOXED_TEXT' | 'CHECKBOX' | 'PICTURE' | 'SIGNATURE'; + type ExpectedToolType = 'TEXT' | 'COMB_TEXT' | 'CHECKBOX' | 'PICTURE' | 'SIGNATURE'; type ExpectedErrorResult = { success: false; diff --git a/react/src/hook.tsx b/react/src/hook.tsx index 84709e0..9fbb6e0 100644 --- a/react/src/hook.tsx +++ b/react/src/hook.tsx @@ -5,7 +5,7 @@ const DEFAULT_REQUEST_TIMEOUT_IN_MS = 30000; type ExtractionMode = 'auto' | 'ocr'; -type ToolType = 'TEXT' | 'BOXED_TEXT' | 'CHECKBOX' | 'PICTURE' | 'SIGNATURE'; +type ToolType = 'TEXT' | 'COMB_TEXT' | 'CHECKBOX' | 'PICTURE' | 'SIGNATURE'; type ErrorCodePrefix = 'bad_request' | 'unexpected' | 'forbidden';