import { PortalAccent } from '../core/index.js'; import { FormSummary, RuleExpression } from '../core/index.js'; import { Uuid } from '../core/index.js'; /** The runner's own words (its buttons and its few sentences), so a translated form is translated all the way. */ interface FormRunnerWords { next: string; back: string; submit: string; sending: string; start: string; thankYou: string; continue: string; /** "press" before the Enter key's name. */ press: string; /** "{n} of {total}" — the progress count. */ of: string; required: string; invalidEmail: string; invalidPhone: string; invalidLink: string; takingYouThere: string; /** The answer-again link's words. */ changeAnswers: string; saveChanges: string; /** Said under the ending once her answers landed. */ received: string; /** The answer-again and finish-later links' button, before and after the copy. */ copyMyLink: string; linkCopied: string; /** When the browser will not copy: the link follows these words to copy by hand. */ copyByHand: string; /** The finish-later lane: saving, saved (then "to finish on another device…"), picked up, start over. */ saving: string; savedFinishElsewhere: string; pickedUp: string; startOver: string; /** Under a payment notice: the answers were kept whatever the payment did. */ answersKept: string; } declare const FORM_RUNNER_WORDS_EN: FormRunnerWords; /** * THE RUNNER'S WORDS IN THE LANGUAGES A FORM IS MOST OFTEN TRANSLATED INTO. A language not listed * here keeps English for the runner's own buttons until the owner writes them (`FormLanguage.words`), * and the builder says which ones it has. */ declare const FORM_RUNNER_WORDS: Readonly>; /** The languages the builder offers by name. Any BCP 47 code is accepted; these are the ones it lists. */ declare const FORM_LANGUAGE_NAMES: Readonly>; /** One question's words in one language. */ interface FormQuestionWords { ask?: string | null; help?: string | null; /** A choice's label by its KEY — the key stays the answer. */ choices?: Record | null; } /** ONE TRANSLATION of a form's words. */ interface FormLanguage { /** A BCP 47 language code, lower-case: `es`, `pt-br`. */ code: string; /** How the language names itself on the page's switcher. Left out, the builder's name for it. */ label?: string | null; title?: string | null; intro?: string | null; submitLabel?: string | null; thankYou?: { title?: string | null; body?: string | null; } | null; welcome?: { title?: string | null; body?: string | null; buttonLabel?: string | null; } | null; /** By the question's field key. */ questions?: Record | null; /** By the ending's id. */ endings?: Record | null; /** The runner's own words, over the built-in ones for this language. */ words?: Partial | null; } /** A language code as the store keeps it: lower-case, `_` read as `-`, or null when it cannot be one. */ declare function formLanguageCode(raw: unknown): string | null; /** The name a language goes by on the page: its own label, the builder's name for it, or its code. */ declare function formLanguageName(lang: Pick): string; /** `presentation.languages` from the screen's words. A translation with no code is dropped. */ declare function formLanguagesDocument(languages: ReadonlyArray | null | undefined): Array>; /** `presentation.languages` back into the screen's words. */ declare function formLanguagesFromDocument(raw: unknown): FormLanguage[]; /** * WHICH TRANSLATION A VISITOR MEETS. The link's `?lang=` first (exactly, then by its first part), * then each language the browser asks for in its own order (`Accept-Language`). `null` is the form * as written — also the answer when the visitor asks for the base language itself. */ declare function pickFormLanguage(presentation: Record | null | undefined, requested: string | null | undefined, acceptLanguage?: string | null): string | null; /** The base language's code and name (`presentation.language`), English when the owner never said. */ declare function formBaseLanguage(presentation: Record | null | undefined): { code: string; label: string; }; /** The runner's words for a language: the built-in ones (English when there are none) under the owner's own. */ declare function formRunnerWords(code: string | null | undefined, own?: Partial | null): FormRunnerWords; /** What a public form is, as `custom.form_public` answers it — only the parts translation touches. */ interface TranslatableForm { title: string; presentation: Record; fields: ReadonlyArray>; } /** * THE FORM IN ONE LANGUAGE: the same document with every word the translation has put over the base * form's. A word the translation leaves out keeps the base form's. The keys, the conditions, the * jumps, the points and the choices' keys are never touched, so the store judges the same form. * `code` null (or a language the form does not have) answers the form unchanged. */ declare function formInLanguage(form: T, code: string | null | undefined): T & { language: string | null; words: FormRunnerWords; }; /** * THE WORDS FOR EACH STATE, SAID ONCE, FOR EVERY SCREEN THAT SHOWS A FORM. * * `FormsPanel` and `FormBuilder` both put a form in front of a person and both * offer the one act that changes this (`custom.anon_publish`). Two copies of * these four sentences is two places for them to drift, and a person who reads * "Not published" on one screen and something else on the other has been lied * to by one of them. The store owns the state; this owns the sentence. */ declare const FORM_STATE_WORDS: Record; /** How a form is put in front of a person. */ declare const FORM_FLOWS: readonly ["one-at-a-time", "single-page"]; type FormFlow = (typeof FORM_FLOWS)[number]; /** One question. It ASKS FOR one Field of the subject Table — never its own column. */ interface FormQuestionSpec { /** The Field's key on the subject Table. */ field: string; /** Ask it in the form's own words. Left out, the Field's own label is the question. */ ask?: string | null; /** One line under the question. */ help?: string | null; /** Override the Field's own `required`. Left out, the Field decides. */ required?: boolean | null; /** * CONDITIONAL LOGIC, AS A RULE. `{"op":"eq","args":[{"field":""},{"const":"Yes"}]}` * — the store answers it, this package never does. An UNDECIDED answer (the * store's `null`, which is what an unanswered question gives) SHOWS the * question: a person is never silently skipped past something. */ showIf?: RuleExpression | null; /** * LOGIC JUMPS (lane TYPEFORM-DUP). After this question, the FIRST jump whose `when` the store * answers true is taken — to a later question or to one of the form's endings. A jump with no * `when` is "always". Undecided falls through to the next question. The store judges the route * (`custom._form_route`); this package never does. */ jumps?: FormJump[] | null; /** SCORING: what each answer is worth (`{"Yes": 5}`); a list answer adds each of its parts. */ points?: Record | null; } /** One logic jump: when the Rule is true, go to a later question or to an ending. */ interface FormJump { when?: RuleExpression | null; to: { question: string; } | { ending: string; }; } /** * ONE ENDING (lane TYPEFORM-DUP). A form may have several; a jump picks one, or the first whose * condition holds (`when`, `scoreMin`, `scoreMax`), or else the first with no condition. */ interface FormEnding { /** A short name, lower-case letters, digits, - and _. Jumps point at it. */ id: string; title?: string | null; body?: string | null; /** One of the organization's public pictures (the builder's side). */ pictureFileId?: Uuid | null; /** The picture's public address (the store resolves it for the public page). */ pictureUrl?: string | null; buttonLabel?: string | null; /** A secure page on the organization's own sites — the thank-you's own rule. */ redirectUrl?: string | null; when?: RuleExpression | null; scoreMin?: number | null; scoreMax?: number | null; } /** THE WELCOME SCREEN, before question one: a title, a message, a picture and a Start button. */ interface FormWelcome { title?: string | null; body?: string | null; pictureFileId?: Uuid | null; pictureUrl?: string | null; buttonLabel?: string | null; } /** The look. Tokens, so light and dark both come out right without a second theme. */ interface FormTheme { /** A design-system accent class or a CSS color. Left out, the app's own accent. */ accent?: string | null; align?: "left" | "center" | null; /** One of design-system `FORM_FONTS`. */ font?: string | null; /** One of design-system `FORM_BUTTON_STYLES`. */ button?: string | null; /** One of design-system `FORM_BACKGROUNDS`. */ background?: string | null; /** The picture for the `picture` background: a public picture of the organization. */ backgroundFileId?: Uuid | null; /** Its public address, as the store resolved it for the public page. */ backgroundUrl?: string | null; } /** * THE FORM'S OWN LOOK, in the portal's words (`custom._portal_style` resolves both): a color of * the app's own palette and two of the organization's PUBLIC pictures. Each part left out is the * organization's look — its newest open portal's, then its own name and logo. `custom.form_declare` * refuses a color the app does not have or a picture a stranger could not see, by name. */ interface FormLook { accent?: PortalAccent | null; logoFileId?: Uuid | null; coverFileId?: Uuid | null; } /** The screen after sending: the form's words, and optionally where to go next. */ interface FormThankYou { title?: string | null; body?: string | null; /** A secure page on one of the organization's own sites. The store judges it. */ redirectUrl?: string | null; } /** THE COMPLETE DECLARATIVE SPEC. One `record_write` and the form exists. */ interface FormSpec { name: string; /** The Table a submission lands in. */ subject: Uuid; intro?: string | null; questions: FormQuestionSpec[]; flow?: FormFlow | null; theme?: FormTheme | null; /** Color, logo and cover a stranger sees. Left out, the organization's look. */ look?: FormLook | null; /** * WHAT THE PERSON SEES AFTER SENDING. `title` and `body` are the form's own * words for the next step ("You are all set" / "Next, book your first visit"). * `redirectUrl` optionally sends them on to a page instead — and only ever to * one of the organization's own sites: `custom.form_declare` refuses anything * else by name, and `custom.form_public` stops handing it out the moment its * site leaves the organization's list (lane S7-PRIME). */ thankYou?: FormThankYou | null; submitLabel?: string | null; /** A separate first screen with a Start button (lane TYPEFORM-DUP). */ welcome?: FormWelcome | null; /** Several endings, each picked by the answers (lane TYPEFORM-DUP). */ endings?: FormEnding[] | null; /** * HIDDEN FIELDS: names read from the link (`?utm_source=…&ref=…`), kept on the submission and * never shown. Not questions — a question with the same key is prefilled instead. */ hiddenFields?: string[] | null; /** * Is this form meant to be answerable by someone who is not signed in? The * runner does NOT make that true by itself — it needs the anonymous write * door, and until that door exists it says so BY NAME rather than writing as * whoever happens to be signed in and calling it public. */ isPublic?: boolean; /** * THE OWNER'S LOGO AND NAME ABOVE THE FORM (lane TYPEFORM-2). Left out, the organization's knob * `forms/show_owner_header` (default on); false hides the agency's brand on a client's form. */ showOwnerHeader?: boolean | null; /** A single choice goes on to the next question when picked. Left out, knob `forms/choice_auto_advance`. */ autoAdvance?: boolean | null; /** NOTIFY ON ANSWER, the builder's words (lane TYPEFORM-2): a webhook and the automation that sends it. */ notify?: FormNotify | null; /** * CD-PAGE-08 — TRANSLATIONS of the form's words (Typeform's multi-language forms). The page picks one by the * link's `?lang=` or the visitor's browser; the answers land under the same keys whatever language was asked. */ languages?: FormLanguage[] | null; /** The language the form is written in. Left out, English. */ language?: { code: string; label?: string | null; } | null; /** * CD-PAGE-08 — ANSWER AGAIN. On, the person who sent an answer is given a private link that opens their own * answers and changes the record they became (`custom.form_response_update`), while the form is open. */ allowEdit?: boolean | null; /** * CD-PAGE-08 — A CALCULATED SCORE ON THE ROW. One of the table's own (number) columns; the score the answers' * points add up to is written there by the store on every send and every change. `{{score}}` recalls it. */ scoreField?: string | null; } /** Where an answer is sent besides the owner's own subscription (lane TYPEFORM-2). */ interface FormNotify { webhookUrl?: string | null; /** The `custom.automation_declare` row that posts to `webhookUrl` (trigger form_answered). */ webhookAutomationId?: Uuid | null; /** * EMAIL THE RESPONDENT (TYPEFORM-2), off unless set: one email to the address the person typed * in `respondentField` (an email question), worded from the ending reached — sent by * `custom.form_respondent_copy` through the platform's notification sender. */ emailRespondent?: boolean | null; respondentField?: string | null; } /** * A form as `custom.forms` reports it — the spec a screen renders, plus the * things only the STORE knows about it. * * The four store-side ids are carried on purpose. `custom.form_declare` is a * RE-STATEMENT of the whole form, so a save that left `notifyRuleId` out would * set it to null and the owner would quietly stop being told when somebody * answers — the exact silent failure the notify Rule exists to prevent. They are * read here and handed straight back by `formDeclareArgs`. */ interface SavedForm extends FormSpec { id: Uuid; /** draft · open · closed · full — the store's own word, never derived on a screen. */ state: FormSummary["state"]; /** The form's readable name in the store. `form_declare` keeps it across saves. */ slug: string; /** Stops taking answers at this many. Null is no ceiling. */ submissionCap: number | null; /** The accept Rule: what counts as a complete answer. Null holds every answer for a person. */ quarantineRuleId: Uuid | null; /** The subscription Rule that tells somebody an answer arrived. */ notifyRuleId: Uuid | null; } /** * The spec as `custom.anon_form.presentation` holds it. * * `questions` is deliberately NOT here: the door writes it itself from the * argument it validated (`presentation || {"questions": p_questions}`), so the * words a form is asked in and the keys it is allowed to accept can never be two * different lists. */ declare function formPresentation(spec: FormSpec): Record; /** `presentation.notify` back into the screen's words. */ declare function notifyFromDocument(raw: unknown): FormNotify | null; /** * WHAT A QUESTION REFUSES BEFORE IT GOES ON (lane TYPEFORM-2) — Typeform's words, one per case. * `null` is "may go on". A required question with no answer is refused; an answer of the email, * phone or link kind is refused when it cannot be one. The store judges again on send. */ declare function answerRefusal(kind: string, required: boolean, value: unknown, words?: Pick): string | null; /** * ANSWER PIPING — Typeform's recall (lane TYPEFORM-2). `{{key}}` or `{{Question words}}` in a later * question, a help line or an ending becomes that question's answer as the person gave it; a * choice reads as its label. Unanswered, it becomes nothing. */ declare function pipeAnswers(text: string, answers: Readonly>, questions: ReadonlyArray<{ key: string; ask?: string | null; }>, labelOf?: (key: string, value: unknown) => string | null, /** CD-PAGE-08: values the form works out (`score`), recalled by name when no question has that key. */ computed?: Readonly>): string; /** `presentation.theme` from the screen's words: camelCase out, the store's snake_case in. */ declare function themeDocument(theme: FormTheme | null): Record; declare function themeFromDocument(raw: unknown): FormTheme | null; declare function welcomeDocument(welcome: FormWelcome | null): Record | null; declare function welcomeFromDocument(raw: unknown): FormWelcome | null; declare function endingDocument(ending: FormEnding): Record; declare function endingsFromDocument(raw: unknown): FormEnding[]; /** The hidden fields' values out of a link's query, narrowed to the names the form declares. */ declare function hiddenFromLink(hiddenFields: readonly string[] | null | undefined, params: Record): Record; /** Does this form route at all — conditions, jumps, endings or points the store must judge? */ declare function formRoutes(form: Pick): boolean; /** HOW A FORM GOES INTO SOMEBODY ELSE'S PAGE. */ declare const FORM_EMBEDS: readonly ["inline", "popup", "slider"]; type FormEmbed = (typeof FORM_EMBEDS)[number]; /** * THE SNIPPET THE OWNER COPIES (lane TYPEFORM-DUP). `inline` is a plain iframe of the public link * in embed mode; `popup` and `slider` are one button plus the platform's own embed script * (`/embed/form.js`), which opens the same iframe in a centered window or a side panel. * Nothing in either needs a cookie: the form keeps its place in its own storage and posts to its * own origin, so it keeps working with third-party cookies blocked. */ declare function formEmbedSnippet(kind: FormEmbed, formUrl: string, options?: { label?: string | null; height?: number | null; }): string; /** `presentation.look` both directions. Nothing set is `null`, one spelling on both sides. */ declare function formLookDocument(look: FormLook | null): Record | null; declare function formLookFromDocument(raw: unknown): FormLook | null; /** * THE THANK-YOU SCREEN, BOTH DIRECTIONS, IN ONE PLACE. The store holds it as * `presentation.thank_you = {title, body, redirect_url}`; the screen reads it as * `FormThankYou`. A thank-you with nothing in it is `null`, so "nothing set" * has one spelling on both sides. */ declare function thankYouDocument(thankYou: FormThankYou | null): Record | null; /** `presentation.thank_you` as the store (or `custom.form_public`) answers it, for a screen. */ declare function thankYouFromDocument(raw: unknown): FormThankYou | null; /** The exact arguments `custom.form_declare` is called with, for a new form or a re-statement. */ declare function formDeclareArgs(form: FormSpec | SavedForm): { table_id: Uuid; title: string; questions: FormQuestionSpec[]; presentation: Record; submission_cap: number | null; quarantine_rule_id: Uuid | null; notify_rule_id: Uuid | null; form_id: Uuid | null; slug: string | null; }; /** One row of `custom.forms` back into the spec a screen and an agent both read. */ declare function formFromSummary(summary: FormSummary): SavedForm; /** * THE SOURCE STAMP every submission carries. A record that arrived through a * form is not indistinguishable from one somebody typed into the grid: it says * which form, which version of it, when, and who was acting. `_source` is a * stamp on the document beside the answers, so it travels with the record * through export, History and the agent's context without a parallel table. */ declare function submissionStamp(args: { formId: Uuid | null; formName: string; actor: string; onBehalfOf?: string | null; }): Record; interface PublicBindingShape { render_mode: string; resource_id: Uuid; /** What the door says the address holds. */ kind: string | null | undefined; /** The table a list reads: the table itself, a view's subject, a record's table. */ table_id: Uuid | null | undefined; } type PublicRead = { read: "table"; tableId: Uuid; } | { read: "view"; tableId: Uuid; viewId: Uuid; } | { read: "record"; recordId: Uuid; } | { read: "unknown"; }; declare function publicReadOf(binding: PublicBindingShape): PublicRead; interface TypedAnswers { /** Ready for the door. */ values: Record; /** * The sentences, in the order the page asks its questions, or null when * every answer became a value. Each one names its Field in plain words. */ refusal: string | null; /** Field key → its sentence, so a screen can point at the question. */ byKey: Record; } /** * Coerce what a public page collected against the Fields that page published. * * `fields` is exactly what `custom.form_public` / `custom.booking_public` * already hand back — the Field definitions of the questions being asked — so * nothing new is read and no door is opened to work this out. */ declare function typedAnswersFor(fields: Array> | null | undefined, values: Record): TypedAnswers; export { FORM_EMBEDS, FORM_FLOWS, FORM_LANGUAGE_NAMES, FORM_RUNNER_WORDS, FORM_RUNNER_WORDS_EN, FORM_STATE_WORDS, answerRefusal, endingDocument, endingsFromDocument, formBaseLanguage, formDeclareArgs, formEmbedSnippet, formFromSummary, formInLanguage, formLanguageCode, formLanguageName, formLanguagesDocument, formLanguagesFromDocument, formLookDocument, formLookFromDocument, formPresentation, formRoutes, formRunnerWords, hiddenFromLink, notifyFromDocument, pickFormLanguage, pipeAnswers, publicReadOf, submissionStamp, thankYouDocument, thankYouFromDocument, themeDocument, themeFromDocument, typedAnswersFor, welcomeDocument, welcomeFromDocument }; export type { FormEmbed, FormEnding, FormFlow, FormJump, FormLanguage, FormLook, FormNotify, FormQuestionSpec, FormQuestionWords, FormRunnerWords, FormSpec, FormThankYou, FormTheme, FormWelcome, PublicBindingShape, PublicRead, SavedForm, TranslatableForm, TypedAnswers };