import { BrandContext, ContentItem, CustomFieldDef, CustomFieldDraft, FieldRules, MailingListAutomation, MailingListDef, MailingListDraft, MailingListTargetConfig, ThankYouPageDef, ThankYouPageDraft, PopupModal } from '../schema/index.ts'; import { EmbedSnippet } from './components/PublishPopover'; import { ImageSearchResult } from './imageSearch'; import { GalleryManager } from './galleryManager'; import { Lang } from './i18n'; export interface FormEditorProps { /** * The form to edit, as a PopupModal JSON object. This is a controlled value: * pass a new object identity to load a different form into the editor. Edits * are surfaced through `onChange` rather than mutating this prop. */ form: PopupModal; /** * Called after every edit with the full, updated form. This is the single * source of truth the host persists or feeds to the renderer. */ onChange?: (form: PopupModal) => void; /** * Called with the current form when the host clicks "Publish" in the preview * toolbar. The editor takes no action itself — the host decides what * publishing means. The button only appears when this is provided. */ onPublish?: (form: PopupModal) => void; /** * The markup the author copies from the popover that follows Publish. The * embed is the host's: its script tag, its element, and its own id for the * form (a host storing forms in its own database knows the form by that id, * not by `form.id`). * * embedSnippet: '\n' + * '' * * A string is copied as-is, with `{{id}}` filled in from the form. A function * is called each time the popover opens and may return a promise, which is * what a new form needs: publishing it is what saves it, and the save is what * mints the id the snippet has to carry. * * embedSnippet: async (form) => { * const id = await saveToMyDatabase(form); * return ``; * } * * Doing the save here means `onPublish` is optional — the Publish button * appears for either one. Use both only if the save belongs in `onPublish` * and the id is already known by the time the snippet is asked for; otherwise * the form is saved twice. * * Pass `null` for no popover at all, for a host with its own publish * confirmation. Unset shows a placeholder snippet. */ embedSnippet?: EmbedSnippet; /** * Show the developer-facing "View JSON" button in the preview toolbar. Meant * for developers, not end users — defaults to on only when running on * localhost, so embedders' end users never see it. Pass an explicit boolean to * override (e.g. always show it in a staging tool). */ showJson?: boolean; /** * Show the Submission "Endpoint URL" and "Method" fields. Hosts that own the * submit endpoint themselves (and set it on the form programmatically) can pass * `false` to hide them, so authors don't edit the target. The rest of the * Submission section (success/error handling) stays. Defaults to `true`. */ showEndpoint?: boolean; /** * UI language for the editor chrome. Defaults to 'en'. 'he' also flips the * layout to right-to-left. Does not affect the form content the host edits. */ lang?: Lang; /** Visual theme for the editor chrome. Defaults to 'light'. */ theme?: 'light' | 'dark'; /** * Open the editor as an overlay dialog instead of filling its container: a * dimmed backdrop over the host's page with the editor centered on it, 80% of * the viewport in each direction on desktop and full-bleed on a small screen. * Defaults to `false` — the editor is a plain block that fills whatever box * the host gives it. * * The overlay is rendered inline (fixed-positioned) rather than portalled to * `document.body`: inside the web component's shadow root a portal would leave * its styles behind. */ modal?: boolean; /** * Called when the author asks to close a `modal` editor — the X in its corner * or a click on the backdrop. Like `onPublish`, the editor takes no action of * its own: the host decides what closing means (unmount it, or pass * `modal={false}`). Without a handler there is nothing to close *to*, so the X * and the click-outside are left out entirely. Ignored when not `modal`. * * The close chip spins in place the moment it's clicked, entirely on its own * — no prop to pass back. It stops the instant the host unmounts the editor, * so a prompt close just looks right. If the host keeps the dialog mounted * (the close failed, or it never intended to unmount), the chip gives up on * itself after a few seconds and hands the X back rather than spin forever. */ onClose?: () => void; /** * Accent color for the editor chrome (buttons, active tabs, focus rings) as * any CSS color. Overrides the built-in default. Only affects the editor UI. */ accent?: string; /** * Optional gradient (any CSS ``) for the filled surfaces only — primary * buttons and the active tab. Text, borders, and focus rings still use the * solid `accent`, which also acts as the fallback here. */ accentGradient?: string; /** * The host's own fields, shown in the layout picker under "Your fields". Each * becomes a pre-filled input whose submit `key` is the host's. Passing this * (or `onCreateField`) puts the editor in integration mode. */ customFields?: CustomFieldDef[]; /** * Called when an author creates a field in the picker. The host persists it * and resolves with the finalized def (real `key`); the editor shows a loader * until then. When omitted, new fields are added locally with a slugged key. */ onCreateField?: (draft: CustomFieldDraft) => Promise; /** * Constraints on the built-in input types, keyed by type: how many of each a * form may have, the submit key new ones are created with, whether the author * may edit that key, and whether one is pinned to every form. * * fieldRules={{ * email: { max: 1, key: 'email', lockKey: true, pinned: true }, * tel: { max: 1, key: 'phone', lockKey: true, creatable: false }, * }} * * `creatable: false` takes the type out of the "create new field" form, for a * type the host supplies as a `customFields` entry of its own. * * A pinned type is seeded into any form that arrives without it (which fires * `onChange`), and its card loses the delete button. The same flags are * available per host field on {@link CustomFieldDef}, and those win where both * apply. Rules govern what the *author* can do; a form assembled * programmatically can still break them, which `validatePopup` reports. */ fieldRules?: FieldRules; /** * The mailing lists the host's backend exposes. Passing these (together with * `mailingListTarget`) surfaces an "Automations" section in Settings where the * author can add/remove a submitter from a list on submit. Each choice * compiles into a hidden submit target — the visitor never sees it. */ mailingLists?: MailingListDef[]; /** * Where mailing-list automations are sent and how they're encoded. Required * for the Automations section to appear (there's nowhere to send without it). */ mailingListTarget?: MailingListTargetConfig; /** * Called when an author creates a mailing list in the Automations section. The * host persists it and resolves with the finalized def (real `id`); the editor * shows a loader until then. When omitted, new lists are added locally with a * slugged id. Mirrors `onCreateField`. Providing this (even without * `mailingLists`) is enough to surface the Automations section. */ onCreateMailingList?: (draft: MailingListDraft) => Promise; /** * Called when an author presses "create page" on a redirect automation. The * thank-you page belongs to the host's app — its domain, its template — so the * host creates it and resolves with the finalized def (`id`, `label`, `url`); * the editor shows a loader until then and writes the returned URL into the * redirect. Passing it is what puts the button there: without a handler there * is nobody to create anything. * * The draft carries the form's id, name and language, plus any success copy * the author already wrote, so the page can be seeded rather than blank. */ onCreateThankYouPage?: (draft: ThankYouPageDraft) => Promise; /** * The thank-you pages the host's app already has. Used to name the page a * redirect is attached to. Optional: the attachment stored on the form carries * a label of its own, so an editor mounted without this list still shows which * page a form points at. When both exist this list wins, being the fresher. */ thankYouPages?: ThankYouPageDef[]; /** * Image search for the Card image gallery. Called with the author's query and * resolves with matching images. Passing it turns the "Image URL" field into a * "Browse gallery" picker. The host owns the provider and its key — proxy * Pexels/Unsplash/etc. server-side rather than shipping a key to the browser. */ onSearchImages?: (query: string) => Promise; /** * Images the gallery shows before the author has typed: the results the host * asked for through the element's `preloadGallery()`. Without them the gallery * opens on its "search to get started" prompt, as it always has. */ galleryDefaults?: ImageSearchResult[]; /** * The full media-library gallery (folders, upload, search) for the Card * image field. Additive to `onSearchImages` — see element.tsx. */ onGalleryManager?: GalleryManager; /** * The host business's identity (Creaditor resolves it from the minted token). * Its `primaryColor` becomes the submit button's fill: any submit button that * hasn't been given a color of its own gets this one written into the form, so * the published JSON carries it and the storefront renderer needs no business * context. Without it, buttons stay the built-in black. * * Because it's written into the form, loading a form whose button had no color * fires an `onChange` with the color filled in. */ brand?: BrandContext; /** * Fired when a data field (an input, or a hidden value) is added to the form — * in the layout picker or via a host edit. `field` is the new content item; * its submit key is `field.onSubmitRequest?.key`. Layout-only blocks (heading, * text, spacer) don't fire this. Notification only — the edit is already in the * `onChange` form. */ onFieldAdd?: (field: ContentItem) => void; /** Fired when a data field is removed from the form. Counterpart to `onFieldAdd`. */ onFieldRemove?: (field: ContentItem) => void; /** * Fired when a data field already on the form is edited in place — renamed, * its options rewritten or reordered, made required or private, given a * different submit key. `field` is the item after the edit and `previous` is * the same item before it, so a host can tell *what* changed. * * This is the counterpart to `onCreateField` for everything that happens * after: the create form is the last point a host hears about a field's * definition, and an author does most of their option-writing on the canvas * afterwards. Match the two by `field.fieldKey` (the host field the item came * from, which survives a rename) or by `field.onSubmitRequest?.key`. * * Moving a field, resizing it, or restyling its kind does not fire this — see * `UNWATCHED_FIELD_KEYS`. Notification only; the edit is already in the * `onChange` form. */ onFieldUpdate?: (field: ContentItem, previous: ContentItem) => void; /** * Fired when the author adds a mailing-list automation (or switches its * action, which reads as a remove of the old + add of the new). Requires * `mailingListTarget` so the automation can be decoded. Notification only. */ onAutomationAdd?: (automation: MailingListAutomation) => void; /** Fired when a mailing-list automation is removed. Counterpart to `onAutomationAdd`. */ onAutomationRemove?: (automation: MailingListAutomation) => void; } export declare function FormEditor({ form, onChange, onPublish, embedSnippet, showJson, showEndpoint, lang, theme, modal, onClose, accent, accentGradient, customFields, onCreateField, fieldRules, mailingLists, mailingListTarget, onCreateMailingList, onCreateThankYouPage, thankYouPages, onSearchImages, galleryDefaults, onGalleryManager, brand, onFieldAdd, onFieldRemove, onFieldUpdate, onAutomationAdd, onAutomationRemove, }: FormEditorProps): import("react").JSX.Element;