@getquick/content 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @getquick/content
2
2
 
3
+ The runtime's public `search({ search, page?, perPage?, language? })` searches
4
+ delivered pages, posts, declared collections and the language's front page
5
+ from the publication store alone. It returns a `found.content` page with
6
+ `items`, `total`, `page`, `perPage`, `totalPages`, or `unavailable.failure`.
7
+ See the [search contract and adoption guide](../docs/search.md).
8
+
3
9
  The configured content runtime for WordPress HTML and WPGraphQL. This slice
4
10
  provides pure helpers, the configured WordPress client, durable delivery and
5
11
  installed SQL migrations, and event/refresh HTTP handlers. It is released as a
@@ -290,3 +296,84 @@ content runtime, framework or WordPress client. Passing
290
296
  at delivery and stores gq-config's Legal Contract and GA4 settings in each
291
297
  language's chrome; `runtime.consentConfiguration(chrome)` returns the page's
292
298
  consent configuration. See the repository's `docs/consent.md`.
299
+
300
+ ## Design presentation and composition
301
+
302
+ Sites import the canonical presentation and theme APIs from `@getquick/content`.
303
+ The package bundles its pinned Design renderer and contract; Sites do not install
304
+ those packages themselves. Apply `gqd-content` to body and chrome regions and emit
305
+ `designThemeStyleSheet(theme)` followed by `presentationStyleSheet(theme)` once
306
+ per response, using the delivered `theme` from entry/home/chrome. Resolve defaults
307
+ with `resolveDesignTheme` for older stored publications. A changed theme requires
308
+ regenerating both stylesheets, including its media thresholds.
309
+
310
+ `presentationHtml(name, shell, presentation, { attributes, model? })` decorates an
311
+ existing root shell. Pass the complete block attributes for native Group layout
312
+ context. `presentationVariables` takes the same attributes context. Both delegate
313
+ validation and responsive rules to the bundled renderer; saved variables survive
314
+ when parsed attributes are absent. An optional extended `model` must also be
315
+ passed to the corresponding canonical stylesheet generator.
316
+
317
+ `renderDesignPattern` restores native text/list structures, media and layout
318
+ wrappers, resolved Navigation and Query composition. Site `options.blocks`
319
+ renderers retain precedence. Complete saved media HTML stays authoritative;
320
+ source-only Image nodes use `renderDesignImage`. The public low-level HTML
321
+ adapters also support parsed Site adapters without a second presentation parser.
322
+
323
+ Carousel uses `renderDesignCarousel(attributes, orderedChildren, shell?)`, where
324
+ children carry `{ name, attributes, html }` and each `html` is the complete rendered
325
+ child. Supply exactly one navigation child, even though WordPress saves its HTML
326
+ as null. `renderDesignCarouselItem` accepts its rendered foreground children and
327
+ an optional saved figure shell. Emit `designCarouselStyle()` once. Video Hero
328
+ uses `renderDesignVideoHero` and the canonical `videoHeroStyleSheet()`; deferred
329
+ iframes remain `data-src` until consent and motion preferences allow playback.
330
+ Navigation uses `navigationStyleSheet(theme)` for interaction geometry.
331
+
332
+ Browser lifecycle is isolated in `@getquick/content/design/client`, exporting
333
+ `mountDesignCarousels`, `mountVideoHero` and `mountDesignNavigation`. Each returns
334
+ cleanup; mount again after navigation replaces the DOM. Importing the root does
335
+ not require browser globals. The configured runtime gates generated Hero media
336
+ before composing already-delivered children, so consent hooks are not rewritten
337
+ a second time.
338
+
339
+ For a producer-resolved Query, preload outside the synchronous renderer:
340
+
341
+ ```ts
342
+ const loaded = await runtime.preloadDesignQueryPosts(pattern, siteOrigin);
343
+ const html = runtime.renderDesignPattern(pattern, {
344
+ siteOrigin,
345
+ resolvePost: loaded.resolvePost,
346
+ });
347
+ ```
348
+
349
+ This uses the existing `publishedEntry(item.uri)` read for standard Post/Page
350
+ items. Its optional `databaseId` and canonical `designBlocks` become a typed
351
+ `DesignPostContext` through `designPostContext`. Older publications without the
352
+ neutral tree cannot supply that context. Custom post types require an explicit
353
+ authorized preloaded record through `preloadDesignQueryPosts(pattern, load)`;
354
+ this API adds no custom post type GraphQL field or query selection engine.
355
+ Delivered item order, count and duplicates are preserved without applying Query
356
+ arguments again. Synced references add no layout wrapper, and Post Content
357
+ recursively renders the matching preloaded neutral blocks.
358
+
359
+ Native decorative Cover backgrounds use the same explicit Site lifecycle:
360
+
361
+ ```ts
362
+ import { mountDesignCoverVideos } from "@getquick/content/design/client";
363
+ const cleanup = mountDesignCoverVideos(document);
364
+ // Invoke cleanup before removing/replacing the rendered page; remount afterward.
365
+ ```
366
+
367
+ The configured publication delivery and `presentationHtml("core/cover", ...)`
368
+ remove eager `autoplay` from direct native Cover background videos. The initializer
369
+ plays when reduced motion is off, pauses when enabled, resumes when disabled,
370
+ and pauses/removes its preference listener on cleanup. It does not own foreground
371
+ or ordinary content videos. If a Site renders raw Cover HTML outside these paths,
372
+ apply `enhanceDesignCoverVideosHtml(html)` before SSR. Sources, posters and ordered
373
+ foreground remain authoritative; no legacy appearance properties are inferred.
374
+
375
+ `mountDesignNavigation` also enhances native Polylang dropdowns inside
376
+ `.wp-block-polylang-language-switcher`, including ordinary published HTML whose
377
+ inline controller was removed during delivery. It navigates to the delivered
378
+ selected option's HTTP(S) URL; it does not resolve languages or evaluate inline
379
+ scripts. Cleanup removes that handler and restores the original selection.
@@ -1,6 +1,6 @@
1
1
  import { S as SERVICE_CATALOG, T as copyLanguage, c as grantedCategories, d as promptReason, f as readConsentCookie, x as OPTIONAL_CATEGORIES } from "./state-BYWc5TUB.js";
2
- import { a as CONSENT_COPY, c as legalRevisions, r as escapeHtml } from "./embeds-p9SyGNZI.js";
3
- import { t as CONSENT_UI_VERSION } from "./version-ChwS3fUu.js";
2
+ import { a as CONSENT_COPY, c as legalRevisions, r as escapeHtml } from "./embeds-CJJemAmE.js";
3
+ import { t as CONSENT_UI_VERSION } from "./version-D81MsCZU.js";
4
4
  import { t as CONSENT_CONFIG_ID } from "./client-config-B3LNSOxl.js";
5
5
  import { t as CONSENT_PATH } from "./paths-Cmzzof3v.js";
6
6
  //#region frontend/src/consent/astro.ts
@@ -121,13 +121,26 @@ function loadEmbed(placeholder, id) {
121
121
  if (src.protocol !== "https:" || embedService(src) !== id) return;
122
122
  const iframe = document.createElement("iframe");
123
123
  for (const [name, value] of Object.entries(embed.attrs)) if (EMBED_ATTRIBUTES.has(name.toLowerCase()) && typeof value === "string") iframe.setAttribute(name, value);
124
- iframe.src = src.href;
125
124
  const original = placeholder.cloneNode(true);
126
- placeholder.replaceChildren(iframe);
127
- placeholder.setAttribute("data-gq-loaded", "");
128
- placeholder.removeAttribute("role");
129
- placeholder.removeAttribute("aria-label");
130
- originals.set(placeholder, original);
125
+ if (placeholder.hasAttribute("data-gq-decorative") && iframe.classList.contains("gqd-video-hero__video")) {
126
+ iframe.dataset.src = src.href;
127
+ for (const name of [
128
+ "data-gq-consent-embed",
129
+ "data-gq-category",
130
+ "data-gq-embed",
131
+ "data-gq-decorative"
132
+ ]) if (placeholder.hasAttribute(name)) iframe.setAttribute(name, placeholder.getAttribute(name));
133
+ iframe.setAttribute("data-gq-loaded", "");
134
+ placeholder.replaceWith(iframe);
135
+ originals.set(iframe, original);
136
+ } else {
137
+ iframe.src = src.href;
138
+ placeholder.replaceChildren(iframe);
139
+ placeholder.setAttribute("data-gq-loaded", "");
140
+ placeholder.removeAttribute("role");
141
+ placeholder.removeAttribute("aria-label");
142
+ originals.set(placeholder, original);
143
+ }
131
144
  }
132
145
  const originals = /* @__PURE__ */ new WeakMap();
133
146
  function unloadEmbed(placeholder) {
@@ -1,7 +1,8 @@
1
1
  import { t as bearerMatches } from "./bearer-DMOCoSci.js";
2
2
  import { i as DECISION_VALIDITY_MS, l as isConsentId, t as CONSENT_ACTIONS, x as OPTIONAL_CATEGORIES } from "./state-BYWc5TUB.js";
3
- import { a as parseConsentRecordRequest } from "./records-Dumrz759.js";
3
+ import { a as parseConsentRecordRequest, t as MAX_RECORD_BYTES } from "./records-Dumrz759.js";
4
4
  import { t as CONSENT_PATH } from "./paths-Cmzzof3v.js";
5
+ import { n as readBoundedBody, t as isSameOrigin } from "./request-guards-BUPr_vA4.js";
5
6
  //#region frontend/src/consent/server.ts
6
7
  const MAX_RETENTION_DAYS = 1826;
7
8
  const DAY_MS = 864e5;
@@ -73,42 +74,8 @@ function createConsentServer(config) {
73
74
  const now = config.now ?? Date.now;
74
75
  const origins = new Set((config.siteOrigins ?? []).map((origin) => new URL(origin).origin));
75
76
  /** CN-R9: same-origin only. A browser's cross-site post carries its Origin. */
76
- function sameOrigin(request) {
77
- const origin = request.headers.get("Origin");
78
- const site = request.headers.get("Sec-Fetch-Site");
79
- if (site && site !== "same-origin") return false;
80
- if (!origin) return false;
81
- if (origin === new URL(request.url).origin || origins.has(origin)) return true;
82
- try {
83
- return new URL(origin).host === request.headers.get("Host");
84
- } catch {
85
- return false;
86
- }
87
- }
88
- async function readBody(request) {
89
- if (Number(request.headers.get("Content-Length") ?? "0") > 2048) return null;
90
- if (!request.body) return "";
91
- const reader = request.body.getReader();
92
- const chunks = [];
93
- let size = 0;
94
- for (;;) {
95
- const { done, value } = await reader.read();
96
- if (done) break;
97
- size += value.byteLength;
98
- if (size > 2048) {
99
- await reader.cancel();
100
- return null;
101
- }
102
- chunks.push(value);
103
- }
104
- const bytes = new Uint8Array(size);
105
- let offset = 0;
106
- for (const chunk of chunks) {
107
- bytes.set(chunk, offset);
108
- offset += chunk.byteLength;
109
- }
110
- return new TextDecoder().decode(bytes);
111
- }
77
+ const sameOrigin = (request) => isSameOrigin(request, origins);
78
+ const readBody = (request) => readBoundedBody(request, MAX_RECORD_BYTES);
112
79
  async function insert(db, request) {
113
80
  const recordedAt = now();
114
81
  return (await db.prepare(`INSERT OR IGNORE INTO consent_records (consent_id, site_id, recorded_at, decided_at, expires_at,
package/dist/consent.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as SERVICE_IDS, D as hostMatches, E as embedService, O as hostService, S as SERVICE_CATALOG, T as copyLanguage, _ as consentConfiguration, a as consentCookie, b as COPY_LANGUAGES, c as grantedCategories, d as promptReason, f as readConsentCookie, g as MEASUREMENT_ID, h as CONSENT_COPY_REVISION, i as DECISION_VALIDITY_MS, k as isServiceId, l as isConsentId, m as serviceGranted, n as CONSENT_COOKIE, o as decide, p as serializeConsentState, r as CONSENT_COOKIE_MAX_AGE_SECONDS, s as denyAll, t as CONSENT_ACTIONS, u as parseConsentState, v as needsBanner, w as catalogService, x as OPTIONAL_CATEGORIES, y as CONSENT_CATEGORIES } from "./state-BYWc5TUB.js";
2
2
  import { a as parseConsentRecordRequest, i as isRecordToken, n as consentRecordRequest, r as isLegalRoleName, t as MAX_RECORD_BYTES } from "./records-Dumrz759.js";
3
- import { a as CONSENT_COPY, c as legalRevisions, i as rewriteEmbeds, l as parseLegalLanguage, n as embedPlaceholder, o as LEGAL_ROLES, r as escapeHtml, s as REQUIRED_LEGAL_ROLES, t as blockedPlaceholder } from "./embeds-p9SyGNZI.js";
4
- import { t as CONSENT_UI_VERSION } from "./version-ChwS3fUu.js";
3
+ import { a as CONSENT_COPY, c as legalRevisions, i as rewriteEmbeds, l as parseLegalLanguage, n as embedPlaceholder, o as LEGAL_ROLES, r as escapeHtml, s as REQUIRED_LEGAL_ROLES, t as blockedPlaceholder } from "./embeds-CJJemAmE.js";
4
+ import { t as CONSENT_UI_VERSION } from "./version-D81MsCZU.js";
5
5
  export { CONSENT_ACTIONS, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_COOKIE_MAX_AGE_SECONDS, CONSENT_COPY, CONSENT_COPY_REVISION, CONSENT_UI_VERSION, COPY_LANGUAGES, DECISION_VALIDITY_MS, LEGAL_ROLES, MAX_RECORD_BYTES, MEASUREMENT_ID, OPTIONAL_CATEGORIES, REQUIRED_LEGAL_ROLES, SERVICE_CATALOG, SERVICE_IDS, blockedPlaceholder, catalogService, consentConfiguration, consentCookie, consentRecordRequest, copyLanguage, decide, denyAll, embedPlaceholder, embedService, escapeHtml, grantedCategories, hostMatches, hostService, isConsentId, isLegalRoleName, isRecordToken, isServiceId, legalRevisions, needsBanner, parseConsentRecordRequest, parseConsentState, parseLegalLanguage, promptReason, readConsentCookie, rewriteEmbeds, serializeConsentState, serviceGranted };
@@ -0,0 +1,163 @@
1
+ //#region frontend/src/forms/state.ts
2
+ /** The query parameter that marks the redirect back after a post. */
3
+ const FORM_STATE_PARAM = "gq-form";
4
+ /** Seconds the outcome is kept for the page after the redirect. */
5
+ const FORM_STATE_MAX_AGE = 300;
6
+ /** Cookies over this many bytes drop the typed values, then Gravity Forms' messages. */
7
+ const MAX_COOKIE_BYTES = 3800;
8
+ const formStateCookieName = (formId) => `gq-form-${formId}`;
9
+ function encode(value) {
10
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
11
+ let binary = "";
12
+ for (const byte of bytes) binary += String.fromCharCode(byte);
13
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
14
+ }
15
+ function decode(value) {
16
+ const binary = atob(value.replaceAll("-", "+").replaceAll("_", "/"));
17
+ return JSON.parse(new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0))));
18
+ }
19
+ const STATUSES = /* @__PURE__ */ new Set([
20
+ "sent",
21
+ "invalid",
22
+ "failed",
23
+ "verification",
24
+ "too-many"
25
+ ]);
26
+ /** The Set-Cookie header that carries a form's outcome to the page. */
27
+ function formStateCookie(state, options) {
28
+ const attempts = [
29
+ {
30
+ s: state.status,
31
+ e: state.errors,
32
+ m: state.messages,
33
+ v: state.values,
34
+ c: state.confirmation
35
+ },
36
+ {
37
+ s: state.status,
38
+ e: state.errors,
39
+ m: state.messages,
40
+ c: state.confirmation
41
+ },
42
+ {
43
+ s: state.status,
44
+ e: state.errors
45
+ }
46
+ ];
47
+ let value = "";
48
+ for (const attempt of attempts) {
49
+ value = encode(attempt);
50
+ if (value.length <= MAX_COOKIE_BYTES) break;
51
+ }
52
+ return `${formStateCookieName(state.formId)}=${value}; Path=/; Max-Age=300; HttpOnly; SameSite=Lax` + (options.secure ? "; Secure" : "");
53
+ }
54
+ /** The Set-Cookie header that removes a form's outcome once the page has shown it. */
55
+ function clearFormStateCookie(formId, options) {
56
+ return `${formStateCookieName(formId)}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax` + (options.secure ? "; Secure" : "");
57
+ }
58
+ function cookieValue(header, name) {
59
+ for (const part of (header ?? "").split(";")) {
60
+ const index = part.indexOf("=");
61
+ if (index > 0 && part.slice(0, index).trim() === name) return part.slice(index + 1).trim();
62
+ }
63
+ return null;
64
+ }
65
+ const stringRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string")) : {};
66
+ const byField = (value) => Object.fromEntries(Object.entries(stringRecord(value)).flatMap(([key, each]) => /^\d+$/.test(key) ? [[Number(key), each]] : []));
67
+ /**
68
+ * A form's outcome for the page: present only on the redirect back after a
69
+ * post to it (`?gq-form=<id>`), from its cookie. Null otherwise.
70
+ */
71
+ function readFormState(request, formId) {
72
+ let url;
73
+ try {
74
+ url = new URL(request.url);
75
+ } catch {
76
+ return null;
77
+ }
78
+ if (url.searchParams.get("gq-form") !== String(formId)) return null;
79
+ const raw = cookieValue(request.cookie, formStateCookieName(formId));
80
+ if (!raw) return null;
81
+ let value;
82
+ try {
83
+ value = decode(raw);
84
+ } catch {
85
+ return null;
86
+ }
87
+ if (typeof value !== "object" || value === null) return null;
88
+ const record = value;
89
+ if (!STATUSES.has(record.s)) return null;
90
+ return {
91
+ formId,
92
+ status: record.s,
93
+ errors: byField(record.e),
94
+ messages: byField(record.m),
95
+ values: stringRecord(record.v),
96
+ ...typeof record.c === "string" && record.c ? { confirmation: record.c } : {}
97
+ };
98
+ }
99
+ /** Where the handler sends the visitor back to: the page, marked for the form. */
100
+ function formStateLocation(source, formId) {
101
+ const url = new URL(source, "https://site.invalid");
102
+ url.searchParams.set(FORM_STATE_PARAM, String(formId));
103
+ url.hash = `gq-form-${formId}`;
104
+ return `${url.pathname}${url.search}${url.hash}`;
105
+ }
106
+ //#endregion
107
+ //#region frontend/src/forms/copy.ts
108
+ const FORMS_COPY = {
109
+ en: {
110
+ required: "required",
111
+ requiredFields: "Fields marked with * are required.",
112
+ errorSummary: "There was a problem with your submission. Please review the fields below.",
113
+ errors: {
114
+ required: "This field is required.",
115
+ email: "Enter a valid email address.",
116
+ "email-mismatch": "The email addresses don't match.",
117
+ number: "Enter a number.",
118
+ range: "Enter a number in the allowed range.",
119
+ url: "Enter a valid web address, starting with https://.",
120
+ choice: "Choose one of the options.",
121
+ "too-long": "This is longer than allowed."
122
+ },
123
+ failed: "Your message couldn't be sent. Please try again later.",
124
+ verification: "We couldn't verify that you're not a robot. Please try again.",
125
+ tooMany: "Too many submissions. Please try again later.",
126
+ sent: "Thank you. Your message has been sent.",
127
+ unavailable: "This form isn't available.",
128
+ submit: "Submit",
129
+ privacyPolicy: "Privacy policy",
130
+ honeypot: "This field is for validation purposes and should be left unchanged.",
131
+ needsJavaScript: "This form needs JavaScript to verify you're not a robot."
132
+ },
133
+ "pt-PT": {
134
+ required: "obrigatório",
135
+ requiredFields: "Os campos assinalados com * são obrigatórios.",
136
+ errorSummary: "Houve um problema com o envio. Reveja os campos abaixo.",
137
+ errors: {
138
+ required: "Este campo é obrigatório.",
139
+ email: "Introduza um endereço de email válido.",
140
+ "email-mismatch": "Os endereços de email não coincidem.",
141
+ number: "Introduza um número.",
142
+ range: "Introduza um número dentro do intervalo permitido.",
143
+ url: "Introduza um endereço web válido, começado por https://.",
144
+ choice: "Escolha uma das opções.",
145
+ "too-long": "Excede o comprimento permitido."
146
+ },
147
+ failed: "Não foi possível enviar a sua mensagem. Tente novamente mais tarde.",
148
+ verification: "Não foi possível verificar que não é um robô. Tente novamente.",
149
+ tooMany: "Demasiados envios. Tente novamente mais tarde.",
150
+ sent: "Obrigado. A sua mensagem foi enviada.",
151
+ unavailable: "Este formulário não está disponível.",
152
+ submit: "Enviar",
153
+ privacyPolicy: "Política de privacidade",
154
+ honeypot: "Este campo serve para validação e deve ser deixado em branco.",
155
+ needsJavaScript: "Este formulário precisa de JavaScript para verificar que não é um robô."
156
+ }
157
+ };
158
+ /** The copy language for a page's `lang`: Portuguese for `pt*`, else English. */
159
+ function formsCopyLanguage(language) {
160
+ return /^pt\b/i.test(language ?? "") ? "pt-PT" : "en";
161
+ }
162
+ //#endregion
163
+ export { clearFormStateCookie as a, formStateLocation as c, FORM_STATE_PARAM as i, readFormState as l, formsCopyLanguage as n, formStateCookie as o, FORM_STATE_MAX_AGE as r, formStateCookieName as s, FORMS_COPY as t };
@@ -0,0 +1,143 @@
1
+ //#region frontend/src/forms/descriptor.d.ts
2
+ /** The Site path the forms handler answers under: `/gq/forms/<formId>`. */
3
+ declare const FORMS_PATH = "/gq/forms";
4
+ /** The descriptor's wire version; another version doesn't parse. */
5
+ declare const FORM_DESCRIPTOR_SCHEMA = 1;
6
+ /** The Gravity Forms block a Site's dispatcher routes to the renderer. */
7
+ declare const GRAVITY_FORMS_BLOCK = "gravityforms/form";
8
+ /** Field types this release renders and submits. */
9
+ declare const SUPPORTED_FIELD_TYPES: readonly ["text", "textarea", "email", "phone", "number", "website", "select", "radio", "checkbox", "consent", "name", "hidden", "html", "section"];
10
+ type FormFieldType = (typeof SUPPORTED_FIELD_TYPES)[number];
11
+ interface FormChoice {
12
+ text: string;
13
+ value: string;
14
+ selected: boolean;
15
+ }
16
+ /** One HTML input of a field: Gravity Forms' `input_<field>` or `input_<field>_<n>`. */
17
+ interface FormInput {
18
+ /** Gravity Forms' input id (`1`, `1.3`). */
19
+ id: string;
20
+ /** The name it is posted as. */
21
+ name: string;
22
+ label: string;
23
+ /** For a checkbox input, the choice's value. */
24
+ value?: string;
25
+ placeholder?: string;
26
+ /** Name sub-inputs: autocomplete token (given-name, family-name…). */
27
+ autocomplete?: string;
28
+ }
29
+ interface FormConsent {
30
+ /** The text next to the checkbox. */
31
+ checkboxLabel: string;
32
+ /** The consent's longer text, as plain text. */
33
+ description: string;
34
+ /** Gravity Forms' form revision the text belongs to; submitted with the consent. */
35
+ revisionId: string;
36
+ }
37
+ interface FormField {
38
+ id: number;
39
+ type: FormFieldType;
40
+ label: string;
41
+ /** Plain text. */
42
+ description: string;
43
+ required: boolean;
44
+ /** A hidden field is submitted, not shown; a `hidden` visibility field is too. */
45
+ visibility: "visible" | "hidden";
46
+ placeholder?: string;
47
+ maxLength?: number;
48
+ defaultValue?: string;
49
+ /** select, radio, checkbox. */
50
+ choices?: FormChoice[];
51
+ /** The inputs it posts (one for most fields). */
52
+ inputs: FormInput[];
53
+ /** consent only. */
54
+ consent?: FormConsent;
55
+ /** html only: sanitized by WordPress (wp_kses_post) on the CMS. */
56
+ content?: string;
57
+ /** number only. */
58
+ range?: {
59
+ min?: number;
60
+ max?: number;
61
+ };
62
+ /** The Site's CSS classes the editor set on the field. */
63
+ cssClass?: string;
64
+ }
65
+ type FormConfirmation = {
66
+ type: "message";
67
+ message: string;
68
+ } | {
69
+ type: "redirect";
70
+ url: string;
71
+ };
72
+ /** Why a form can't be offered as it is; readiness reports each one as a failure. */
73
+ type FormDiagnosticCode = "unavailable" | "unsupported-captcha" | "unsupported-file-upload" | "unsupported-payment" | "unsupported-multi-page" | "unsupported-field" | "unsupported-conditional-logic" | "requires-login";
74
+ interface FormDiagnostic {
75
+ code: FormDiagnosticCode;
76
+ fieldId?: number;
77
+ /** Gravity Forms' field type, for a field diagnostic. */
78
+ type?: string;
79
+ message: string;
80
+ }
81
+ interface FormDescriptor {
82
+ schema: typeof FORM_DESCRIPTOR_SCHEMA;
83
+ id: number;
84
+ /** False when the form is missing, inactive or trashed: it renders as unavailable. */
85
+ available: boolean;
86
+ title: string;
87
+ /** Plain text. */
88
+ description: string;
89
+ submitLabel: string;
90
+ fields: FormField[];
91
+ /** Gravity Forms' honeypot input, when the form enables it. */
92
+ honeypot: {
93
+ name: string;
94
+ } | null;
95
+ /** The default confirmation. */
96
+ confirmation: FormConfirmation;
97
+ /** Set by the CMS for fields and settings this release doesn't support. */
98
+ diagnostics: FormDiagnostic[];
99
+ }
100
+ /**
101
+ * A descriptor as the CMS delivered it, keeping only the public fields above,
102
+ * or null when it isn't one. Fields of a type this release doesn't support
103
+ * are left out; the CMS reports them as diagnostics.
104
+ */
105
+ declare function parseFormDescriptor(value: unknown): FormDescriptor | null;
106
+ /**
107
+ * The descriptors a publication carries (the CMS's `getquickForms`, as JSON
108
+ * text or data), dropping any that don't parse. Undefined for no value.
109
+ */
110
+ declare function parseFormDescriptors(value: unknown): FormDescriptor[] | undefined;
111
+ /** The form a `gravityforms/form` block shows, from its attributes (`formId`). */
112
+ declare function blockFormId(attributes: Record<string, unknown> | null | undefined): number | null;
113
+ /** A form's descriptor among a publication's, or null. */
114
+ declare function findForm(forms: readonly FormDescriptor[] | null | undefined, formId: number): FormDescriptor | null;
115
+ /** The path a form posts to on the Site's own origin. */
116
+ declare const formAction: (formId: number) => string;
117
+ /** Everything readiness must fail for: an unavailable form, and each unsupported field or setting. */
118
+ declare function formDiagnostics(descriptor: FormDescriptor): FormDiagnostic[];
119
+ /** The spam controls a Site's forms use. */
120
+ type FormSpamControl = "honeypot" | "turnstile";
121
+ interface FormsReadiness {
122
+ ok: boolean;
123
+ forms: Array<{
124
+ id: number;
125
+ title: string;
126
+ ok: boolean;
127
+ diagnostics: FormDiagnostic[];
128
+ /** Gravity Forms' honeypot is enabled on the form. */
129
+ honeypot: boolean;
130
+ }>;
131
+ /** Controls the Site declares that no checked form uses (honeypot only). */
132
+ warnings: string[];
133
+ }
134
+ /**
135
+ * Readiness over the forms a Site's published content references: fails for
136
+ * any diagnostic. `spam` is the Site's declared controls; declaring the
137
+ * honeypot for a form that doesn't enable it is reported as a warning.
138
+ */
139
+ declare function formsReadiness(descriptors: readonly FormDescriptor[], options?: {
140
+ spam?: readonly FormSpamControl[];
141
+ }): FormsReadiness;
142
+ //#endregion
143
+ export { parseFormDescriptors as S, findForm as _, FormConsent as a, formsReadiness as b, FormDiagnosticCode as c, FormInput as d, FormSpamControl as f, blockFormId as g, SUPPORTED_FIELD_TYPES as h, FormConfirmation as i, FormField as l, GRAVITY_FORMS_BLOCK as m, FORM_DESCRIPTOR_SCHEMA as n, FormDescriptor as o, FormsReadiness as p, FormChoice as r, FormDiagnostic as s, FORMS_PATH as t, FormFieldType as u, formAction as v, parseFormDescriptor as x, formDiagnostics as y };