/** When the modal opens. Discriminated union on `type`. */ export type PopupTrigger = { type: 'delay'; seconds: number; } | { type: 'scroll'; percent: number; } | { type: 'immediate'; } /** * Waits for a click instead of a timer. What gets clicked is either the * author's own launcher button — see {@link PopupLauncher} — or, when the site * would rather use a control it already has, any element on the page carrying * `data-creaditor-open="
"`. Both routes are always live, so a form can * ship with our button and still be opened from a menu link. */ | { type: 'click'; }; /** Pre-defined layout template the merchant picks between. */ export type PopupDesign = 'basic' | 'image-behind' | 'image-right' | 'image-left' | 'image-top'; /** How often the popup may re-open (enforced via localStorage). */ export type PopupFrequency = 'session' | 'day' | 'ever' | 'always'; /** * How the popup is placed. `inline` is the default: the form embeds into the * page flow at full width with no overlay, and `mountPopup` appends it inside * the `htmlId` element. `modal` opts into the centered full-page overlay. * * Read it via {@link placementOf} rather than defaulting at each call site. */ export type PopupPlacement = 'modal' | 'inline'; /** * How the popup's body fields flow. `stack` (default) puts each field on its own * row; `row` lays the inputs out horizontally (wrapping) for a wide, single-row * signup bar. Pairs naturally with `inline` placement, but the two are * independent — a modal can use a row form and an inline popup can stack. */ export type PopupFormLayout = 'stack' | 'row'; /** * How far along a step form the visitor is, shown above the fields. `bar` is a * filling track, `count` reads "2 of 4", `dots` is one dot per step. `none` * leaves it out — fine for two steps, less so for ten. */ export type PopupStepProgress = 'none' | 'bar' | 'count' | 'dots'; /** * Step form settings. Present and `enabled` splits the form across screens at * its `page-break` items (see {@link stepsOf}); absent or off renders every * item on one screen, which is what a form without this field has always done. * * The breaks live in `contentItems` rather than here, so switching this off * keeps them: an author trying the mode out and changing their mind gets their * steps back when they switch it on again. */ export interface PopupSteps { enabled: boolean; /** Progress indicator style. Omitted → 'bar'. */ progress?: PopupStepProgress; /** * A `radio` choice advances to the next step on its own, with no Next click. * What makes a form read as a survey rather than as pagination. Omitted → * off, since it's wrong for any step holding more than one field. * * Radio and nothing else. A `checkbox` left unticked is a real answer, so no * moment on it means "done"; a `multi-select`'s first pick isn't its last; * and a `select` fires its change event on arrow-key navigation in several * browsers, which would advance a keyboard user before they had picked. */ autoAdvance?: boolean; /** Wording of the forward button on every step but the last. Omitted → 'Next'. */ nextLabel?: string; /** Wording of the back button on every step but the first. Omitted → 'Back'. */ backLabel?: string; } /** How the popup's image fills its area. `cover` (default) crops to fill; `contain` fits the whole image. */ export type PopupImageFit = 'cover' | 'contain'; /** Where the image sits within its area (CSS background-position keyword). */ export type PopupImagePosition = 'center' | 'top' | 'bottom' | 'left' | 'right'; /** * Text direction of the rendered popup. Independent of the builder UI language — * a merchant running an English builder can still author a right-to-left popup. * * This used to be the language picker too: the builder labelled `ltr` "English" * and `rtl` "Hebrew", and the renderer chose its own words from it. That pairing * has now been split — see {@link PopupLanguage} — and a form that carries a * `language` takes its direction from that. `direction` stays as the fallback, * so every form saved before the split keeps reading and writing the same way. */ export type PopupDirection = 'ltr' | 'rtl'; /** * The language the popup speaks: the one thing that decides the words the * renderer says on its own behalf (validation complaints, the button mid-submit, * Next/Back — see `renderer/strings.ts`) and, through it, the direction the card * is laid out in. * * Authored copy is not translated: labels and headings are whatever the author * typed. This is the language of the frame around them. * * Adding one means a value here, a direction in {@link LANGUAGE_DIRECTION}, and * a column in the renderer's dictionary. */ export type PopupLanguage = 'en' | 'he'; export declare const LANGUAGES: PopupLanguage[]; /** * The language a form is in. A form authored before the language field existed * says it only through its direction, which is exactly what that control meant * at the time, so an `rtl` form reads as Hebrew. */ export declare function languageOf(popup: Pick): PopupLanguage; /** * The direction to lay a form out in. The language decides it when there is one * — the builder writes both together, so they can't drift — and `direction` * answers for everything saved before the split. */ export declare function directionOf(popup: Pick): PopupDirection; /** * The button that opens the popup, for a form whose trigger is `click`. * * The author designs it here rather than the site hand-rolling one, so the thing * that opens the form looks like the form: same font, same rounding, the same * brand fill the submit button takes. The renderer draws it where the form would * have gone and swaps it for the popup when it's pressed. */ export interface PopupLauncher { /** What the button says. Omitted → the renderer's own wording for the language. */ label?: string; /** Fill, text colour, size, width and alignment — the submit button's controls. */ styleProps?: StyleProps; /** Corner rounding in px. Omitted → the card's own {@link PopupModal.borderRadius}. */ borderRadius?: number; } export type ContentType = 'heading' | 'text' | 'spacer' | 'html' | 'page-break' | 'email' | 'tel' | 'date' | 'number' | 'radio' | 'select' | 'multi-select' | 'checkbox' | 'toggle' | 'free-text-input' | 'textarea' | 'hidden' | 'submit-button'; /** * Types whose choices come from {@link ContentItem.options}. `radio` and * `select` submit one option value; `multi-select` submits several (see * {@link MULTI_VALUE_SEPARATOR}). */ export declare const OPTION_TYPES: ContentType[]; export declare function isOptionType(type: ContentType): boolean; /** * Types whose value is a yes/no rather than text. `checkbox` and `toggle` differ * only in how they're drawn — a line of consent versus a settings switch — so * everything downstream (seeding, submitting, the required check) treats them * alike and reads the pair through here. */ export declare const BOOLEAN_TYPES: ContentType[]; export declare function isBooleanType(type: ContentType): boolean; /** * How a `multi-select`'s chosen values are joined when they have to travel as a * single string: the GET query string, and the redirect URL a `forwardValues` * success appends to. A POST body keeps them as a JSON array instead. */ export declare const MULTI_VALUE_SEPARATOR = ","; export interface StyleProps { align?: 'left' | 'center' | 'right'; color?: string; backgroundColor?: string; /** Font size in px. Omitted → the renderer's per-type default. */ fontSize?: number; /** * Font size in px for a field's placeholder text, independent of the size the * answer is typed at. Omitted → the placeholder follows {@link fontSize}, * which is what it does natively. Only text-ish inputs, textareas and a * select's empty row have a placeholder to size. */ placeholderFontSize?: number; /** * How much of the form's column the item takes, in percent. Omitted → the * layout's own default (full width stacked, an even column in a row form). */ width?: number; } export interface PopupOption { label: string; value: string; } /** * Where a field's value lands is derived from the popup's HTTP method, not * configured per field: GET → query string, POST → JSON body. Only the key * (the parameter name) is authored. */ export interface OnSubmitRequest { key?: string; } /** A static extra key/value pair sent with every submit (custom submit callback values). */ export interface CallbackPayloadEntry { key: string; value: string; } /** * One endpoint a submit fires to. A form always has a *primary* target — the * author-facing `url` / `method` / `onSubmitCallbackPayload` on the PopupModal, * edited in the builder's Submission settings. On top of that a host can append * `submitTargets`: extra endpoints the visitor never sees and the author never * edits, added programmatically (typically at publish, off the form object the * host already receives). This is how an "on submit → add/remove from mailing * list" automation is encoded — a hidden target pointed at the host's mailing * endpoint, with the action carried in its `payload`. * * On submit the renderer fires the primary target plus every `submitTargets` * entry. The primary target is authoritative: its response drives `onSuccess` * (coupon code, etc.) and its HTTP status decides success/error. Extra targets * are best-effort — fired in parallel, their failures swallowed — so a flaky * automation endpoint never costs you the lead the primary target captured. */ export interface SubmitTarget { /** Stable id, so a host can find and update its own target on re-publish. */ id: string; url: string; method: 'GET' | 'POST'; /** * Static extra key/value pairs merged into *this* target's request only — * where a host encodes the automation, e.g. `{ key: 'action', value: 'add' }` * plus `{ key: 'list', value: 'newsletter' }`. Same URL-safe key rule as * {@link CallbackPayloadEntry}. */ payload?: CallbackPayloadEntry[]; /** * Developer-owned target: not rendered or editable in the builder UI, fired * silently on submit. Extra targets are hidden by default; the flag exists so * a host can also add a *visible* secondary target if it ever wants one. */ hidden?: boolean; /** * `false` marks the target as a *declaration* the renderer must not call: the * host reads it off the stored form JSON and acts on it server-side when the * submission arrives. Defaults to `true` (the renderer fires it from the * browser). Declared targets may leave `url` empty — nothing dials it. * * This is what keeps a mailing-list automation off the wire: a client-fired * target exposes both the endpoint and the (list, action) pair to anyone with * devtools, who can then subscribe or unsubscribe whoever they like. A host * that already stores the form can act on it server-side instead, where the * request is authenticated and the automation isn't forgeable. */ fireFromClient?: boolean; /** Optional human label, shown only in developer views (JSON / debug). */ label?: string; } export interface ContentItem { id: string; order: number; type: ContentType; value?: string; placeholder?: string; height?: number; rows?: number; /** * `number` only — the range a value must fall in. Either may stand alone: a * `min` of 1 with no `max` is "at least one", which is most of what bounds * are for. They reach the rendered input as its `min`/`max` attributes, so * the browser's own steppers respect them, *and* they're checked on submit — * a visitor can always type past a spinner (see {@link firstOutOfRange}). * * Left as plain numbers rather than a `step`: the field submits whatever was * typed, decimals included, so bounding a price to 0–99.99 works without * anyone having to reason about step arithmetic. */ min?: number; max?: number; /** * How many columns of the form's {@link GRID_COLUMNS}-wide grid this item * takes. Omitted → the full width, which is what every item did before spans * existed. Two fields at 6 sit side by side; three at 4 make a row of thirds. * * Rows are not stored: the body is a CSS grid, so items flow onto one line * while their spans fit and wrap when they don't. That keeps `contentItems` a * flat, ordered list — the same one drag-to-reorder splices — with no row * ids to keep in sync. Read it through {@link spanOf}, which also carries * older forms that authored a percentage {@link StyleProps.width}. */ span?: number; styleProps?: StyleProps; options?: PopupOption[]; required?: boolean; /** * Inputs only. Hides the input from the rendered popup while it keeps * submitting, which is what the builder calls the hidden checkbox. The key is * named `private` from before the `hidden` content type existed; the name * stays for forms already saved with it, but nothing user-facing says it. * In the builder the item still appears, dimmed, so authors can configure it. * * The flag only hides. The value it carries comes from the page URL, which is * not this flag's doing: every input is seeded from the query string (see the * renderer's `seedValuesFromUrl`), and a hidden one simply has no other way * to be filled. */ private?: boolean; onSubmitRequest?: OnSubmitRequest; /** * The host field this item was created from ({@link CustomFieldDef.key}). * Written by the field picker and carried in the saved form, so the builder * can tell which host field an item belongs to even after the author renames * its label. Field rules (lock the key, cap the count, keep it undeletable) * resolve through this. Items added from the generic input types don't have * one; their rules resolve by `type` instead. */ fieldKey?: string; } /** * The input kinds a host can expose as a custom field. Each maps onto a * `ContentType` through {@link customFieldContentType} — only `text` is renamed * (to `free-text-input`); the rest carry their name straight over. */ export type CustomFieldType = 'text' | 'textarea' | 'email' | 'tel' | 'date' | 'number' | 'radio' | 'select' | 'multi-select' | 'checkbox' | 'toggle'; /** * The same list at runtime. `CustomFieldType` is erased at build, so a host * passing `customFields` from its own backend has nothing checking the `type` it * sends — and an unrecognised one used to travel all the way through * {@link customFieldContentType} into a `ContentItem` the renderer can't draw. */ export declare const CUSTOM_FIELD_TYPES: CustomFieldType[]; export declare function isCustomFieldType(type: unknown): type is CustomFieldType; /** What the author fills in the "create new field" form, before the host finalizes it. */ export interface CustomFieldDraft { label: string; type: CustomFieldType; options?: PopupOption[]; required?: boolean; } /** * A finalized custom field. The host owns `key` — it lands verbatim in the * submit request, so the host assigns and validates it (URL-safe). `description` * is optional microcopy shown under the field in the picker. */ export interface CustomFieldDef extends CustomFieldDraft { key: string; id?: string; description?: string; /** * The submit key is the host's, not the author's: the editor hides the key * row entirely, so what the host assigned is what the form sends. Use it for * the fields your backend matches on by name. The key is still in the form * JSON, and in the developer JSON view. */ lockKey?: boolean; /** * How many times this field may be added to one form. Omitted means once — * the field submits under one key, so a second copy would just collect the * same answer twice. The picker greys the field out once the cap is reached. * Set a number above 1 for a field that's genuinely repeatable. */ max?: number; /** * The field is always required: the item is created (and any loaded form * normalized) with `required: true`, and the editor drops the Required toggle * so there's nothing to switch off. For the fields your backend can't take a * blank value for. */ lockRequired?: boolean; /** * The field can't be made private, so the visitor always sees it and types a * real answer. The editor drops the Private toggle, and clears the flag on a * loaded form that has it set. Pair with {@link lockRequired} for a field that * must genuinely be collected. */ lockPrivate?: boolean; /** * The field is part of every form: it's seeded into a form that doesn't have * it (on load, and after a template swap) and the author can't remove it. * Implies `max: 1` unless a larger `max` is set. */ pinned?: boolean; /** * The integration needs this field to do its job, but the author still * decides. While the form doesn't have it, the Layout tab shows a prompt with * a one-click Add, and the field is highlighted in the picker. Nothing is * blocked: a form without it saves and publishes normally. * * This is the softer half of {@link pinned}: use it for "submissions only * reach the mailing platform if there's an email here", where forbidding the * author is wrong but leaving them to guess is worse. */ recommended?: boolean; /** * Why the field matters, in the host's own words: "Needed to add contacts to * SendMsg." Shown in the prompt and under the field in the picker. Falls back * to generic copy when omitted. */ recommendedHint?: string; /** * What this field *is* to the host, shown as a chip on the item's card next to * its content type: "SendMsg", "CRM email". It tells an author that the item * isn't a loose text box but the thing their system reads. * * Defaults to the field's own `label`, so a chip appears for every host field * without configuring anything; set this when the label alone doesn't say * which system the field belongs to. */ roleLabel?: string; } /** * A constraint on one of the built-in input types, for hosts that don't pass * {@link CustomFieldDef}s but still need a fixed shape: "exactly one email * field, its key is always `email`, and it can't be deleted". The flags mirror * the ones on a custom field. * * Rules are keyed by {@link ContentType} and count *every* item of that type, * including ones added from a custom field that maps onto it. */ export interface FieldRule { /** How many items of this type a form may have. Omitted means no cap. */ max?: number; /** The submit key new items of this type are created with. */ key?: string; /** The submit key is the host's: the editor hides the key row on this type. */ lockKey?: boolean; /** Items of this type are always required; the Required toggle is dropped. */ lockRequired?: boolean; /** Items of this type can't be made private; the Private toggle is dropped. */ lockPrivate?: boolean; /** * One item of this type is seeded into every form and can't be removed. * Implies `max: 1` unless a larger `max` is set. */ pinned?: boolean; /** * The integration wants this type on the form, but the author decides. Shows * a prompt with a one-click Add while it's missing, and highlights the type in * the picker. Nothing is blocked. See {@link CustomFieldDef.recommended}. */ recommended?: boolean; /** Why it matters, in the host's own words. Shown in the prompt. */ recommendedHint?: string; /** * What items of this type are to the host, shown as a chip on the card. A * type rule has no label to fall back on, so the chip appears only when this * is set. See {@link CustomFieldDef.roleLabel}. */ roleLabel?: string; /** * `false` drops this type from the "create new field" form's type list, so an * author can't mint a field of it. For the types the host's own backend owns * (email, phone): it supplies them as `customFields`, and a second one the * author invented would submit under a key the backend doesn't know. * * Only the create form is affected — a host field of the type is still * addable, and so is a rule's `pinned` seed. Defaults to creatable. */ creatable?: boolean; } /** Per-type field rules, as passed to the editor's `fieldRules` prop. */ export type FieldRules = Partial>; /** The `ContentType` a custom field renders as. `text` is our free-text input. */ export declare function customFieldContentType(type: CustomFieldType): ContentType; /** A mailing list the host exposes to the author. `id` lands in the request. */ export interface MailingListDef { id: string; label: string; /** Optional microcopy shown under the list in the picker. */ description?: string; } /** * What the author fills in the "create new list" form, before the host * finalizes it. The host persists it and returns a {@link MailingListDef} with a * real, validated `id` — mirrors {@link CustomFieldDraft} / `onCreateField`. */ export interface MailingListDraft { label: string; description?: string; } export type MailingListAction = 'add' | 'remove'; /** * How to turn a (list, action) choice into an HTTP request. All keys/values are * host-defined; the defaults suit a simple `{ list, action }` POST body. The * builder reads this to compile automations and to decode them back for editing. */ export interface MailingListTargetConfig { /** * The host endpoint that performs the subscribe/unsubscribe. Required in the * default client-fired mode; leave it out when `fireFromClient` is `false`, * where the host acts on the automation server-side and nothing is dialed * from the browser. */ url?: string; /** * `false` compiles automations as declarations rather than live targets: they * still round-trip through the form JSON (and through * {@link readMailingListAutomations}, so the host's backend can read them), * but the renderer never calls them, and the visitor's submit stays a single * request to the primary endpoint. * * Prefer this whenever the host stores the form JSON. Firing from the client * publishes the mailing endpoint and its (list, action) vocabulary to anyone * who opens devtools, so subscribing or unsubscribing an arbitrary address * becomes a hand-rolled POST. Acting on the automation server-side keeps it * behind whatever authentication the submit endpoint already has. * * Defaults to `true`, which suits a host that doesn't see the submission * server-side at all (a CDN embed posting to a third-party form service). */ fireFromClient?: boolean; /** Defaults to POST. */ method?: 'GET' | 'POST'; /** Payload key carrying the list id. Defaults to 'list'. */ listKey?: string; /** Payload key carrying the action. Defaults to 'action'. */ actionKey?: string; /** Value sent for an "add" action. Defaults to 'add'. */ addValue?: string; /** Value sent for a "remove" action. Defaults to 'remove'. */ removeValue?: string; } /** One author-chosen automation: what to do to which list on submit. */ export interface MailingListAutomation { listId: string; action: MailingListAction; } /** True when a target was produced by {@link compileMailingListTarget}. */ export declare function isMailingListTarget(target: SubmitTarget): boolean; /** * Whether a host's config is complete enough to offer mailing-list automations: * a client-fired config needs somewhere to send, a server-side one doesn't. The * builder gates the Automations section on this. */ export declare function isMailingConfigUsable(config: MailingListTargetConfig | undefined): boolean; /** * Compile one automation into a hidden submit target. Under * `fireFromClient: false` the target is a declaration for the host's backend to * read, so it carries no url and the renderer skips it. */ export declare function compileMailingListTarget(automation: MailingListAutomation, config: MailingListTargetConfig, label?: string): SubmitTarget; /** Recover the author's automations from a popup's compiled submit targets. */ export declare function readMailingListAutomations(popup: PopupModal, config: MailingListTargetConfig): MailingListAutomation[]; /** * Produce the next `submitTargets` array with the mailing-list automations * replaced by `automations` — any non-mailing-list targets a host added are left * untouched. Returns `undefined` when the result is empty so cleared automations * don't linger as an empty array in the exported JSON. */ export declare function withMailingListAutomations(popup: PopupModal, automations: MailingListAutomation[], config: MailingListTargetConfig, labelFor?: (listId: string) => string | undefined): SubmitTarget[] | undefined; export interface EmailAutomation { /** Stable id so rows keep their identity across edits. */ id: string; /** Recipient address(es) — free text; the host decides how to parse/validate. */ to: string; /** Optional subject line. */ subject?: string; } /** A thank-you page the host's app hosts. `url` is what the redirect navigates to. */ export interface ThankYouPageDef { /** The host's id for the page. Round-trips through the form JSON. */ id: string; /** What to call it in the editor. */ label: string; /** Where it lives. Written into the redirect's `url`. */ url: string; } /** * What the builder hands the host when the author asks for a page. The host * creates it and returns a finalized {@link ThankYouPageDef} — the same shape as * `onCreateField` / `onCreateMailingList`. * * It carries the form's own details as well as the name the author typed, so the * host can seed the page with something better than a blank: the copy the author * already wrote for the success screen, in the language the form is in. */ export interface ThankYouPageDraft { /** What the author called it. */ name: string; /** The form the page is for. */ formId: string; formName: string; /** The form's language, so the page can be created in the same one. */ language: PopupLanguage; /** * The success copy the author already composed, as the HTML fragment the * `rich` success stores. Absent when the form has no rich success — which is * the usual case here, since a form that redirects doesn't show one. */ successHtml?: string; } /** * The thank-you page a redirect points at, recorded on the form so the editor * can name it again after a reload. The `url` beside it is what actually * navigates; this is only the provenance. * * The label is stored rather than only the id so the editor can show the * attachment without the host re-supplying its page list on every mount. When * the host *does* supply one, that list wins — it's the fresher of the two. */ export interface AttachedThankYouPage { id: string; label?: string; } export type SubmitSuccess = { type: 'close'; } | { type: 'message'; text: string; autoCloseMs?: number; } | { type: 'coupon'; text?: string; code?: string; codeFromResponsePath?: string; copyable?: boolean; } /** * A rich, author-composed success screen: an HTML fragment produced by the * builder's rich-text editor. Inline coupon components are serialized as * placeholder elements — `` * — that the renderer hydrates into live coupon chips (copy button, code from * the submit response). The HTML is author-trusted but the renderer still walks * it through a tag/attribute whitelist rather than injecting it raw. Supersedes * `message` and `coupon` as the type the builder authors; those two stay in the * union so previously saved forms keep rendering. */ | { type: 'rich'; html: string; } | { type: 'redirect'; url: string; newTab?: boolean; /** * When true, the submitted field values are appended to the redirect URL * as query params (keyed by each field's submit key), so the destination * page can personalize — e.g. greet the visitor by name on a thank-you page. */ forwardValues?: boolean; /** * Set when `url` points at a page the host created from the builder. Pure * provenance: the renderer navigates to `url` and never reads this. See * {@link AttachedThankYouPage}. */ thankYouPage?: AttachedThankYouPage; }; export type SubmitError = { type: 'message'; text: string; } /** Rich, author-composed error copy — same HTML fragment model as the `rich` * {@link SubmitSuccess}, rendered inline under the form. `message` stays for * back-compat and for the renderer's own client-side validation errors. */ | { type: 'rich'; html: string; }; export interface PopupModal { id: string; name: string; url: string; method: 'GET' | 'POST'; trigger: PopupTrigger; design: PopupDesign; /** Page-embedded inline (the default) vs. overlay modal. See {@link placementOf}. */ placement?: PopupPlacement; /** Field flow: stacked (default) vs. wide single-row. Defaults to 'stack'. */ formLayout?: PopupFormLayout; /** * Split the form across screens at its `page-break` items. Omitted → one * screen, which is what every form did before steps existed. See * {@link PopupSteps} and {@link stepsOf}. */ steps?: PopupSteps; /** * The language the popup speaks — the renderer's own words, and the direction * it lays out in. Read it through {@link languageOf} / {@link directionOf} * rather than here, so forms saved before this field keep answering. */ language?: PopupLanguage; /** * Text direction of the popup itself. Defaults to 'ltr' when omitted, and is * overruled by `language` when a form carries one. See {@link directionOf}. */ direction?: PopupDirection; /** * The button that opens the form, for a `click` trigger. Omitted → the * renderer draws its default button, so a click-triggered form always has * something to open it. See {@link PopupLauncher}. */ launcher?: PopupLauncher; borderRadius?: number; /** * The popup's font. Omitted → the built-in default (Arial). `'host'` → inherit * the surrounding website's font (only meaningful on the live popup, which is * mounted in the host document; the editor preview can't see the host's font). * Any other value is a curated Google Font family name (see `src/fonts.ts`), * loaded on demand by the renderer. */ fontFamily?: string; imageUrl?: string; /** Card background fill colour (hex). Omitted → white. */ backgroundColor?: string; /** Alpha 0–1 applied to {@link backgroundColor} (translucent fill). Omitted → opaque. */ backgroundOpacity?: number; /** Opacity 0–1 of the whole card, content included. Omitted → 1. */ cardOpacity?: number; /** Backdrop dim: alpha 0–1 of the overlay behind a modal. Omitted → 0.55. */ backdropOpacity?: number; /** Darkening scrim over the photo in the image-behind layout, 0–1. Omitted → 0.45. */ imageScrimOpacity?: number; /** How the image fills its area. Omitted → 'cover'. */ imageFit?: PopupImageFit; /** Where the image sits within its area. Omitted → 'center'. */ imagePosition?: PopupImagePosition; /** Body padding in px. Omitted → 28. */ padding?: number; /** Card max width, interpreted in `widthUnit` (px count, or a 0–100 percentage of the container). Picking a template writes {@link defaultCardWidth}; omitted falls back to the stylesheet (520, 720 side-by-side, 500 inline). */ width?: number; /** Unit for `width`. Omitted → 'px'. When '%', `width` is a percentage of the container. */ widthUnit?: 'px' | '%'; /** Card minimum height in px. Omitted → 350. */ minHeight?: number; htmlId?: string; /** * The addresses this form runs on. Omitted or empty (the default) means every * page it's loaded on — which is the whole answer when the embed is pasted * wherever the form belongs. It earns its keep the other way round: one script * across a whole site, with the form itself saying where it applies. * * An entry is a page's address, pasted out of a browser * (`https://shop.co.il/pricing`). Ignored when comparing, because none of it * makes a different page: the scheme, a leading `www.`, a trailing slash, the * `#fragment`, and case. A star stands for any run of characters, so an * address ending `/products/` plus a star is every product page. An entry with * no query string ignores the page's. * * A bare path (`/pricing`) is also accepted and matched against the path * alone, which keeps a form working on a staging domain as well as the live * one. * * Read it with `matchesPage(popup, href)` rather than comparing strings at * each call site. */ urls?: string[]; dismissible?: boolean; frequency?: PopupFrequency; onSuccess?: SubmitSuccess; onError?: SubmitError; onSubmitCallbackPayload?: CallbackPayloadEntry[]; /** * Extra, host-added submit endpoints fired alongside the primary `url` — see * {@link SubmitTarget}. Optional; typically appended by the embedding host * (mailing-list automations, webhooks) rather than authored in the builder. */ submitTargets?: SubmitTarget[]; /** * Declarative "send an email on submit" automations. Frontend-only intent — * the embedding host reads these off the form JSON and sends the mail; the * builder and renderer never send anything. See {@link EmailAutomation}. */ emailAutomations?: EmailAutomation[]; contentItems: ContentItem[]; } /** * Content types that collect a value *from the visitor*. `hidden` also * contributes to the submit request but has no visible input, carrying either * the value the author fixed or one seeded from the page URL, so it is * intentionally not listed here. */ export declare const INPUT_TYPES: ContentType[]; export declare const CONTENT_TYPES: ContentType[]; export declare const DESIGNS: PopupDesign[]; export declare const FREQUENCIES: PopupFrequency[]; export declare const DIRECTIONS: PopupDirection[]; /** The attribute a site puts on its own element to open a form from it. */ export declare const OPEN_ATTRIBUTE = "data-creaditor-open"; export declare const PLACEMENTS: PopupPlacement[]; export declare const FORM_LAYOUTS: PopupFormLayout[]; export declare const STEP_PROGRESS: PopupStepProgress[]; export declare const IMAGE_FITS: PopupImageFit[]; export declare const IMAGE_POSITIONS: PopupImagePosition[]; export declare function isInputType(type: ContentType): boolean; /** * Whether the renderer has a way to draw this type. `ContentType` is erased at * build, so this is the only thing standing between a host-assembled form and an * item that renders as nothing — see {@link isCustomFieldType} for the same * problem one level up, at the field a host declares. */ export declare function isContentType(type: unknown): type is ContentType; /** Columns in the form body's grid. Twelve divides by 2, 3, 4 and 6. */ export declare const GRID_COLUMNS = 12; /** A span the grid can actually place: a whole number of columns, 1..12. */ export declare function clampSpan(span: number): number; /** * Most items one line will hold. Twelve columns divide evenly by four, and a * fifth field on a line is too narrow to type into on any realistic card width. */ export declare const MAX_ROW_ITEMS = 4; /** The items grouped as the grid lays them out, one array per rendered line. */ export declare function rowsOf(items: ContentItem[]): ContentItem[][]; /** * The column each item starts on, keyed by id, so a row that doesn't fill the * grid sits centred instead of packed against the leading edge. * * Rows the builder makes always add up to twelve — one item takes the line, two * split it, four take a quarter each — so those come back starting at column 1 * and nothing about them moves. A short row is one a width came in on: an older * form's percentage, or a half left stranded when the field beside it went. Left * to the grid's own packing those sat hard against the leading edge with all the * slack behind them, which reads as a mistake rather than as a narrow field. * * The whole row moves together, not each item on its own: two fields sharing a * short line are a pair, and centring them separately would open a gap down the * middle of something the author put side by side. * * A column is the finest step there is, so slack that won't halve leaves the odd * one over on the trailing side — half a column nearer the edge the row used to * hug, and the only choice the grid can actually express. */ export declare function columnStartsOf(items: ContentItem[]): Map; /** * The columns an item occupies: the share of the line handed to it, or the whole * line when it stands there alone. * * `styleProps.width` is deliberately not read. A percentage used to come back * here as a column count, which is how the Design tab's field-width slider * worked — and it made a mess of it: every field in the form cut to the same * fraction, sat against the leading edge with the rest of the line empty, and * any two that landed next to each other silently became a row. The slider is * gone (see styleGroups), and a width left behind on a form authored while it * existed stays inert rather than going on quietly deciding the layout. * * The submit button is unaffected either way: it renders outside the grid, so * its own width is the one that is still authored, and this is never asked * about it. */ export declare function spanOf(item: ContentItem): number; /** Whether this form is split across screens. */ export declare function stepsEnabled(popup: Pick): boolean; /** * The items grouped into the screens they render on, one array per step. The * `page-break` items are the boundaries and appear in no step: they separate * content rather than being content. * * Always returns at least one step, so a caller never has to special-case a * form with no breaks (it is one step holding everything, which is exactly what * a form renders as with steps switched off). */ export declare function stepsOf(items: ContentItem[]): ContentItem[][]; /** How many screens the form has. */ export declare function stepCount(items: ContentItem[]): number; /** * Which step an item renders on, or `-1` if it isn't in the list. A page break * reports the step it *ends*, which is where the builder shows it. */ export declare function stepIndexOfItem(items: ContentItem[], id: string): number; /** * Where a step sits in the flat list: `start` is its first item's index and * `end` is one past its last, so `items.slice(start, end)` is the step and * `end` is where a new item appended to that step belongs. Both clamp to the * list, so an out-of-range step reports the end of it. */ export declare function stepRangeOf(items: ContentItem[], stepIndex: number): { start: number; end: number; }; /** * A submit key or radio value ends up verbatim in a URL query/body key, so it * must be URL-safe: no spaces, only RFC 3986 unreserved characters * (letters, digits, and `-` `.` `_` `~`). */ export declare const URL_TOKEN_RE: RegExp; export declare function isUrlSafeToken(value: string): boolean; export declare const URL_TOKEN_HINT = "No spaces or special characters: use letters, digits, . _ ~ -"; /** Designs that consume the top-level `imageUrl`. */ export declare function designUsesImage(design: PopupDesign): boolean; /** * The card width a template looks right at, in px. A side-by-side image needs * room for two columns and a row form needs room for its input columns, while * the stacked layouts read better narrow. Picking a template writes this onto * the form, so the author starts from a size that suits the layout and can then * change it. */ export declare function defaultCardWidth(design: PopupDesign, formLayout?: PopupFormLayout): number; /** * The effective placement: `inline` unless the form explicitly asks for a modal. * Both the builder and the renderer read it through here so the default can't * drift between what the author sees and what the visitor gets. */ export declare function placementOf(popup: Pick): PopupPlacement; /** Shorthand for {@link placementOf} === 'inline'. */ export declare function isInlinePlacement(popup: Pick): boolean; /** * Whether the visitor can close the form: the ✕, Esc, and a click on the * overlay all ask here, so the three can't disagree about one card. * * A modal opened by a click is always closable, whatever `dismissible` says. * Everything else honours the flag. The difference is who started it: a form * that arrives on a timer is the site interrupting someone, and a merchant may * legitimately insist they read it. A form behind a button is something the * visitor asked for, and an action a visitor takes has to be one they can take * back — a full-page overlay with no way out, entered by choice, is a trap and * reads as a broken page. * * Inline forms have no overlay to be trapped by, so the flag is theirs alone. */ export declare function isDismissible(popup: Pick): boolean; /** The id the primary (author-facing) submit target carries in {@link effectiveTargets}. */ export declare const PRIMARY_TARGET_ID = "primary"; /** * The full, ordered list of endpoints a submit fires to: the primary target * (built from the popup's own `url` / `method` / `onSubmitCallbackPayload`) * first, then any host-added {@link SubmitTarget}s. The renderer iterates this * so primary and extra targets share one code path; index 0 is always primary * and stays authoritative for success/error. */ export declare function effectiveTargets(popup: PopupModal): SubmitTarget[]; /** * Whether the renderer should actually call this target. `false` marks a target * the host acts on server-side after reading the stored form, so it lives in the * JSON without costing the visitor a request. Developer views (JSON export, * debug) still list it, which is why {@link effectiveTargets} doesn't filter. */ export declare function isClientFired(target: SubmitTarget): boolean;