@aglyn/plugins-forms 1.0.0-beta.196 → 1.0.0-beta.197

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-forms",
3
- "version": "1.0.0-beta.196",
3
+ "version": "1.0.0-beta.197",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,17 +25,17 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.196",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.196",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.196",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.196",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.196",
33
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.196",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.196",
35
- "@aglyn/shared-util-http": "1.0.0-beta.196",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.196",
37
- "@aglyn/shared-util-tools": "1.0.0-beta.196",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.196",
28
+ "@aglyn/aglyn": "1.0.0-beta.197",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.197",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.197",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.197",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.197",
33
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.197",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.197",
35
+ "@aglyn/shared-util-http": "1.0.0-beta.197",
36
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.197",
37
+ "@aglyn/shared-util-tools": "1.0.0-beta.197",
38
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.197",
39
39
  "@swc/helpers": "0.5.23"
40
40
  },
41
41
  "peerDependencies": {
@@ -79,6 +79,37 @@ export interface FormProps {
79
79
  * `__` prefix keeps it out of the submitted fields.
80
80
  */
81
81
  export declare const FIELD_MAP_INPUT_PREFIX = "__map__";
82
+ /**
83
+ * The hidden input the field carrying the consent-group sentence posts its
84
+ * key under (AGL-3320). `__`-prefixed like the map inputs, so it never
85
+ * reaches the submitted fields; the Form lifts it into the request body as
86
+ * `__consentGroup`, where the submit route reads it.
87
+ *
88
+ * Posted by the FIELD, not the Form: the key is proof the sentence was on
89
+ * screen, and the field that rendered it is the only thing that knows it was.
90
+ * A form whose consent field never mounted posts no key, and its opt-in is
91
+ * recorded for the one site the visitor is on.
92
+ */
93
+ export declare const CONSENT_GROUP_INPUT = "__consentGroup";
94
+ /** The site's consent-group sentence, as `/api/consent/disclosure` answers it. */
95
+ export interface FormConsentDisclosure {
96
+ /**
97
+ * The field the form declares as its opt-in, or `null` for a form that
98
+ * declares none — the sentence then sits under whichever field the submit
99
+ * route would read an opt-in from by its name.
100
+ */
101
+ fieldName: string | null;
102
+ /** What the person is told they are agreeing to hear from. */
103
+ text: string;
104
+ /** The key of exactly this sentence, posted back with the submission. */
105
+ key: string;
106
+ }
107
+ /**
108
+ * The route's answer, or `null` when it has no sentence for this site — a
109
+ * site that sends on its own, or an answer that is not the shape it should
110
+ * be. `null` renders nothing and posts no key.
111
+ */
112
+ export declare function readFormConsentDisclosure(body: unknown): FormConsentDisclosure | null;
82
113
  /**
83
114
  * Redirect targets are restricted to same-origin paths (`/thanks`) and
84
115
  * absolute https URLs (AGL-557): stored props reach every visitor's
@@ -28,6 +28,7 @@ import Checkbox from "@mui/material/Checkbox";
28
28
  import FormControl from "@mui/material/FormControl";
29
29
  import FormControlLabel from "@mui/material/FormControlLabel";
30
30
  import FormGroup from "@mui/material/FormGroup";
31
+ import FormHelperText from "@mui/material/FormHelperText";
31
32
  import FormLabel from "@mui/material/FormLabel";
32
33
  import Link from "@mui/material/Link";
33
34
  import MenuItem from "@mui/material/MenuItem";
@@ -36,7 +37,7 @@ import RadioGroup from "@mui/material/RadioGroup";
36
37
  import Rating from "@mui/material/Rating";
37
38
  import Stack from "@mui/material/Stack";
38
39
  import TextField from "@mui/material/TextField";
39
- import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
40
+ import { createContext, forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react";
40
41
  import { BUNDLE_ID } from "../constants/bundle-common.js";
41
42
  import { generatePresetId } from "../utils/generate-preset-id.js";
42
43
  /*
@@ -57,6 +58,39 @@ export const FORM_FIELD_ID = 'formField';
57
58
  * arbitrary nesting between Form and field needs no React context; the
58
59
  * `__` prefix keeps it out of the submitted fields.
59
60
  */ export const FIELD_MAP_INPUT_PREFIX = '__map__';
61
+ /**
62
+ * The hidden input the field carrying the consent-group sentence posts its
63
+ * key under (AGL-3320). `__`-prefixed like the map inputs, so it never
64
+ * reaches the submitted fields; the Form lifts it into the request body as
65
+ * `__consentGroup`, where the submit route reads it.
66
+ *
67
+ * Posted by the FIELD, not the Form: the key is proof the sentence was on
68
+ * screen, and the field that rendered it is the only thing that knows it was.
69
+ * A form whose consent field never mounted posts no key, and its opt-in is
70
+ * recorded for the one site the visitor is on.
71
+ */ export const CONSENT_GROUP_INPUT = '__consentGroup';
72
+ /**
73
+ * The disclosure a Form fetched, handed to the fields inside it. A context
74
+ * because the answer flows DOWN, from the one request the Form makes to the
75
+ * one field it describes; the key flows back up through the DOM, like the
76
+ * dataset mapping does.
77
+ */ const FormConsentDisclosureContext = /*#__PURE__*/ createContext(null);
78
+ /**
79
+ * The route's answer, or `null` when it has no sentence for this site — a
80
+ * site that sends on its own, or an answer that is not the shape it should
81
+ * be. `null` renders nothing and posts no key.
82
+ */ export function readFormConsentDisclosure(body) {
83
+ const value = body != null ? body : {};
84
+ const text = typeof value['text'] === 'string' ? value['text'].trim() : '';
85
+ const key = typeof value['key'] === 'string' ? value['key'].trim() : '';
86
+ if (!text || !key) return null;
87
+ const fieldName = typeof value['fieldName'] === 'string' && value['fieldName'].trim() ? value['fieldName'].trim() : null;
88
+ return {
89
+ fieldName,
90
+ text,
91
+ key
92
+ };
93
+ }
60
94
  /**
61
95
  * Redirect targets are restricted to same-origin paths (`/thanks`) and
62
96
  * absolute https URLs (AGL-557): stored props reach every visitor's
@@ -196,18 +230,55 @@ const REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
196
230
  /** The read-only lockdown's own words (AGL-1511); never a hardcoded line. */ const [pausedMessage, setPausedMessage] = useState('');
197
231
  /** The abuse ceiling's visitor notice (AGL-1666); see `unavailable` below. */ const [unavailable, setUnavailable] = useState(null);
198
232
  const [alerts, setAlerts] = useState([]);
233
+ /*
234
+ * THE CONSENT-GROUP SENTENCE, when this site sends as part of a group
235
+ * (AGL-3320).
236
+ *
237
+ * Asked for on the rendered site only — the canvas has no site to ask
238
+ * about — and handed to the fields, one of which renders it under the
239
+ * opt-in and posts its key. Until the answer arrives, and whenever it has
240
+ * none, nothing renders and nothing is posted: the opt-in is then recorded
241
+ * for this site alone, which is the safe way to be early or wrong.
242
+ */ const [disclosure, setDisclosure] = useState(null);
243
+ useEffect(()=>{
244
+ if (!hostId) return;
245
+ let current = true;
246
+ const query = new URLSearchParams(_extends({
247
+ hostId
248
+ }, formId ? {
249
+ formId
250
+ } : {}));
251
+ siteFetch(`/api/consent/disclosure?${query.toString()}`).then((response)=>response.ok ? response.json() : null).then((body)=>{
252
+ if (current) setDisclosure(readFormConsentDisclosure(body));
253
+ }).catch(()=>{
254
+ if (current) setDisclosure(null);
255
+ });
256
+ return ()=>{
257
+ current = false;
258
+ };
259
+ }, [
260
+ hostId,
261
+ formId,
262
+ siteFetch
263
+ ]);
199
264
  const handleSubmit = useCallback(async (event)=>{
200
265
  event.preventDefault();
201
266
  if (!hostId || status === 'sending') return;
202
267
  const data = new FormData(event.currentTarget);
203
268
  const fields = {};
204
269
  let website = '';
270
+ let consentGroup = '';
205
271
  for (const [key, value] of data.entries()){
206
272
  if (typeof value !== 'string') continue;
207
273
  if (key === 'website') {
208
274
  website = value;
209
275
  continue;
210
276
  }
277
+ // The key the consent field posted beside the sentence it rendered.
278
+ if (key === CONSENT_GROUP_INPUT) {
279
+ consentGroup = value;
280
+ continue;
281
+ }
211
282
  // `__`-prefixed inputs are internal controls — the rating field's
212
283
  // star radios (AGL-544) and each field's dataset-mapping input — and
213
284
  // never submit.
@@ -235,7 +306,9 @@ const REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
235
306
  path: window.location.pathname,
236
307
  fields,
237
308
  website
238
- }, campaignTouchField()))
309
+ }, consentGroup ? {
310
+ [CONSENT_GROUP_INPUT]: consentGroup
311
+ } : {}, campaignTouchField()))
239
312
  });
240
313
  if (response.ok) {
241
314
  // Site alerts from the actions builder (AGL-148).
@@ -359,7 +432,10 @@ const REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
359
432
  revealSelector ? /*#__PURE__*/ _jsx("style", {
360
433
  children: `${revealSelector}{display:none !important}`
361
434
  }) : null,
362
- children,
435
+ /*#__PURE__*/ _jsx(FormConsentDisclosureContext.Provider, {
436
+ value: disclosure,
437
+ children: children
438
+ }),
363
439
  /*#__PURE__*/ _jsx("input", {
364
440
  type: "text",
365
441
  name: "website",
@@ -484,6 +560,27 @@ Form.displayName = 'AglynForm';
484
560
  value: datasetFieldId,
485
561
  readOnly: true
486
562
  }) : null;
563
+ /*
564
+ * THE CONSENT-GROUP SENTENCE, under the field that records the opt-in
565
+ * (AGL-3320): the field the form declares, or — where it declares none —
566
+ * one the submit route reads an opt-in from by its name. The key rides
567
+ * beside it, so a submission carries the key only while the sentence is
568
+ * on screen.
569
+ */ const disclosure = useContext(FormConsentDisclosureContext);
570
+ const describesOptIn = disclosure ? disclosure.fieldName ? disclosure.fieldName === name : Aglyn.isMarketingConsentFieldName(name) : false;
571
+ const consentNote = disclosure && describesOptIn ? /*#__PURE__*/ _jsxs(_Fragment, {
572
+ children: [
573
+ /*#__PURE__*/ _jsx(FormHelperText, {
574
+ children: disclosure.text
575
+ }),
576
+ /*#__PURE__*/ _jsx("input", {
577
+ type: "hidden",
578
+ name: CONSENT_GROUP_INPUT,
579
+ value: disclosure.key,
580
+ readOnly: true
581
+ })
582
+ ]
583
+ }) : null;
487
584
  if (fieldType === 'select') {
488
585
  return /*#__PURE__*/ _jsxs(_Fragment, {
489
586
  children: [
@@ -516,7 +613,8 @@ Form.displayName = 'AglynForm';
516
613
  children: choice
517
614
  }, index))
518
615
  })),
519
- mapInput
616
+ mapInput,
617
+ consentNote
520
618
  ]
521
619
  });
522
620
  }
@@ -540,7 +638,8 @@ Form.displayName = 'AglynForm';
540
638
  })
541
639
  }, index))
542
640
  }),
543
- mapInput
641
+ mapInput,
642
+ consentNote
544
643
  ]
545
644
  }));
546
645
  }
@@ -565,7 +664,8 @@ Form.displayName = 'AglynForm';
565
664
  })
566
665
  }, index))
567
666
  }),
568
- mapInput
667
+ mapInput,
668
+ consentNote
569
669
  ]
570
670
  }));
571
671
  }
@@ -590,7 +690,8 @@ Form.displayName = 'AglynForm';
590
690
  name: name,
591
691
  value: rating != null ? rating : ''
592
692
  }),
593
- mapInput
693
+ mapInput,
694
+ consentNote
594
695
  ]
595
696
  }));
596
697
  }
@@ -609,7 +710,8 @@ Form.displayName = 'AglynForm';
609
710
  size: "small",
610
711
  slotProps: hintSlotProps
611
712
  }, rest)),
612
- mapInput
713
+ mapInput,
714
+ consentNote
613
715
  ]
614
716
  });
615
717
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { sendAnalyticsBeacon } from '@aglyn/aglyn/app-utils/analytics-beacon'\nimport { trackEventBeforeNavigation } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { campaignTouchField } from '@aglyn/aglyn/app-utils/campaign-touch'\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n mdiEmailFastOutline,\n mdiEmailOutline,\n mdiFormTextbox,\n} from '@aglyn/shared-data-mdi'\nimport { isSameOriginPath } from '@aglyn/shared-util-http/safe-redirect'\nimport Alert from '@mui/material/Alert'\nimport Button from '@mui/material/Button'\nimport Checkbox from '@mui/material/Checkbox'\nimport FormControl from '@mui/material/FormControl'\nimport FormControlLabel from '@mui/material/FormControlLabel'\nimport FormGroup from '@mui/material/FormGroup'\nimport FormLabel from '@mui/material/FormLabel'\nimport Link from '@mui/material/Link'\nimport MenuItem from '@mui/material/MenuItem'\nimport Radio from '@mui/material/Radio'\nimport RadioGroup from '@mui/material/RadioGroup'\nimport Rating from '@mui/material/Rating'\nimport Stack from '@mui/material/Stack'\nimport TextField from '@mui/material/TextField'\nimport {\n type FormEvent,\n forwardRef,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n/*\n * The heading and the stack this block's form sits inside are mui elements,\n * so the nodes that carry them say so. A node's `pluginId` names the bundle\n * that REGISTERS its component, not the bundle whose preset placed it.\n *\n * The id comes from the core's constant, as it does for every plugin that\n * composes mui elements: importing the mui plugin for one string would make\n * this plugin unusable without it on the import graph.\n */\nconst { MUI_BUNDLE_ID } = Aglyn\n\n// Component ids are persisted in screen documents; never rename.\nexport const FORM_ID: Aglyn.ComponentId = 'form'\nexport const FORM_FIELD_ID: Aglyn.ComponentId = 'formField'\n\n/** After-submit outcomes (AGL-557); message is the historical default. */\nexport type FormAfterSubmit = 'message' | 'redirect' | 'reveal'\n\nexport interface FormProps {\n /**\n * The form entity this node submits as (`docs/specs/reusable-forms.md` §2c).\n *\n * THE identity. `formName` below is a caption and always was: it is copied\n * onto each submission at write time and reconciled with nothing, so\n * renaming a form split its history in two and two pages sharing a label\n * were one list. An id survives both.\n *\n * This is the whole binding. Reuse across pages needs nothing further —\n * promote the bound subtree once and the id travels inside the definition,\n * so every instance writes the same form's submissions without the author\n * retyping a label. Two instances of one definition are ONE form, which is\n * the point; an author who wants two forms makes two forms.\n */\n formId?: string\n /** Identifies the form in the submissions inbox. */\n formName?: string\n /**\n * Dataset (by id) submissions also append into (AGL-556); the inbox\n * copy is always written. Renaming the dataset never breaks the binding.\n */\n datasetId?: string\n /**\n * Legacy dataset-by-name binding (AGL-141), honored when no `datasetId`\n * is set; kept so persisted nodes keep working.\n */\n datasetName?: string\n /**\n * The dataset binding above, signed by the compose that rendered this form\n * (AGL-2773). The submit route writes a record only where this says, so the\n * page, not the request, decides which dataset a submission reaches. Present\n * on the published site only; the canvas and Preview never submit.\n */\n datasetBindingToken?: string\n submitLabel?: string\n successMessage?: string\n /**\n * What a successful submit does (AGL-557): show the success message\n * (default), redirect the visitor, or reveal a hidden element.\n */\n afterSubmit?: FormAfterSubmit\n /** Redirect target screen — rename-safe id, resolved like ScreenLink. */\n redirectScreenId?: string\n /**\n * Manual redirect target used when no screen is picked: a same-site\n * `/path` or an absolute https URL (everything else is dropped).\n */\n redirectUrl?: string\n /** Node revealed on submit; hidden until then (afterSubmit=reveal). */\n revealNodeId?: string\n children?: React.ReactNode\n}\n\n/**\n * Hidden-input prefix a FormField uses to publish its field → dataset\n * schema-field mapping to the enclosing Form (AGL-556). Rides the DOM so\n * arbitrary nesting between Form and field needs no React context; the\n * `__` prefix keeps it out of the submitted fields.\n */\nexport const FIELD_MAP_INPUT_PREFIX = '__map__'\n\n/**\n * Redirect targets are restricted to same-origin paths (`/thanks`) and\n * absolute https URLs (AGL-557): stored props reach every visitor's\n * browser, so `javascript:`/`data:` URLs — and protocol-relative\n * `//host` forms, which keep the scheme but swap the host — never pass.\n *\n * The relative branch asks {@link isSameOriginPath} rather than reading the\n * string's first two characters (AGL-1881). This site never got the AGL-2486\n * backslash fix, and the character list would not have been enough anyway:\n * the URL parser deletes tab/LF/CR before parsing, so `/<TAB>/evil.com`\n * carries neither `//` nor `\\` and `window.location.assign` — which\n * `formNavigation` below calls — still lands the visitor on `evil.com`. The\n * absolute branch is unchanged: an explicit `https://` target is a documented\n * feature, and for absolute input the parser's verdict was never in doubt.\n */\nexport const sanitizeRedirectUrl = (url?: string): string | undefined => {\n const trimmed = (url ?? '').trim()\n if (!trimmed) return undefined\n if (/^https:\\/\\//i.test(trimmed)) return trimmed\n if (isSameOriginPath(trimmed)) return trimmed\n return undefined\n}\n\n/**\n * Where a redirect outcome sends the visitor: the picked screen's\n * resolved href wins; the manual URL is the fallback, sanitized above.\n * Undefined (deleted screen, bad URL) degrades to the message outcome.\n */\nexport const resolveRedirectTarget = (\n screenHref: string | undefined,\n redirectUrl: string | undefined,\n): string | undefined => screenHref ?? sanitizeRedirectUrl(redirectUrl)\n\n// Reveal ids are interpolated into a CSS selector — restrict to the id\n// alphabet so a stored prop can't smuggle selector syntax (AGL-557).\nconst REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/\n\n/**\n * Navigation seam for the redirect outcome: jsdom's `window.location`\n * is not patchable, so tests stub this indirection instead.\n */\nexport const formNavigation = {\n assign: (url: string) => window.location.assign(url),\n}\n\n/** DOM event dispatched on every successful submit (AGL-557). */\nexport const FORM_SUBMITTED_EVENT = 'aglyn:form-submitted'\n\n/**\n * Report that a bound form was seen, or typed into.\n *\n * Fire-and-forget, and deliberately the same instrument the overlay counters\n * use: `sendBeacon` hands the request to the browser, which delivers it\n * outside the page's lifetime, so nothing here can delay a render or survive\n * long enough to fail visibly.\n *\n * Requires a bound `formId`. An unbound form has no document to count on, and\n * an id is never invented: the collector's `update` would find nothing, which\n * is the right outcome but a pointless request.\n */\nexport function sendFormBeacon(\n hostId: string | undefined,\n formId: string | undefined,\n event: 'view' | 'start',\n): void {\n if (!hostId || !formId) return\n // Best-effort by construction: `sendAnalyticsBeacon` swallows everything and\n // refuses outright from a non-production build or a browser marked ours. A\n // view that does not report is a view this form's completion rate is\n // measured without, which the console says.\n sendAnalyticsBeacon({ hostId, formId, form: event })\n}\n\n/**\n * Lead-capture form (AGL-76): collects its field children's values and\n * posts them to the tenant's `/api/forms/submit` with the site's host id\n * from SiteContext. Without a site context (besigner canvas, preview) the\n * submit is inert, so editing never creates submissions. Includes a\n * honeypot input for naive bots.\n */\nconst Form = forwardRef<HTMLFormElement, FormProps>((props, ref) => {\n const {\n formId,\n formName,\n // Read by compose, which signs them into `datasetBindingToken`; taken out\n // here only so they never reach the DOM through `rest`.\n datasetId: _datasetId,\n datasetName: _datasetName,\n datasetBindingToken,\n submitLabel,\n successMessage,\n afterSubmit,\n redirectScreenId,\n redirectUrl,\n revealNodeId,\n children,\n ...rest\n } = props\n const { hostId } = Aglyn.useSite()\n const siteFetch = Aglyn.useSiteFetch()\n // Resolves the redirect screen like ScreenLink does (rename-safe) and\n // flags editing surfaces, where outcomes must never fire (AGL-557).\n const { href: redirectScreenHref, suppressNavigation } = Aglyn.useScreenLink(\n afterSubmit === 'redirect' ? redirectScreenId : undefined,\n )\n // Reveal target (AGL-557): hidden through a form-owned <style> tag so\n // SSR paints it hidden from the first frame; the style unmounts with\n // the form after a successful submit — which IS the reveal. Editing\n // surfaces keep the target visible so it stays editable.\n const revealSelector =\n afterSubmit === 'reveal' &&\n revealNodeId &&\n REVEAL_NODE_ID_PATTERN.test(revealNodeId) &&\n !suppressNavigation\n ? `[data-aglyn=\"leaf:${revealNodeId}\"]`\n : undefined\n const [status, setStatus] = useState<\n 'idle' | 'sending' | 'sent' | 'error' | 'preview' | 'paused' | 'unavailable'\n >(\n 'idle',\n )\n /*\n * THE VIEW AND THE START — the two facts a completion rate is taken over.\n *\n * Both are reported ONCE per mounted form, from a ref rather than from\n * state: a counter driven by a re-render would be a Firestore write per\n * keystroke, which is the per-render write this must not become. The ref\n * also survives the re-renders `status` causes, so a visitor who edits a\n * field, submits, and is shown an error is one view and one start.\n *\n * Gated on `!suppressNavigation`, the editing-surface flag: the besigner\n * canvas and the console's preview render this component for its author,\n * and counting an author looking at their own draft as a visitor view would\n * put the merchant into their own denominator.\n */\n const reported = useRef({ view: false, start: false })\n useEffect(() => {\n if (suppressNavigation || reported.current.view) return\n reported.current.view = true\n sendFormBeacon(hostId, formId, 'view')\n }, [hostId, formId, suppressNavigation])\n\n /**\n * A START is the first edit anyone makes to this form.\n *\n * On the form's own `input`, which bubbles from every field — no listener\n * registration, no per-field wiring, and nothing to keep in sync when a\n * field type is added. Focus would be the looser reading and the wrong one:\n * a form that is tabbed through and abandoned was never started, and\n * counting it would inflate the denominator abandonment is measured over.\n */\n const handleFirstInput = useCallback(() => {\n if (suppressNavigation || reported.current.start) return\n reported.current.start = true\n sendFormBeacon(hostId, formId, 'start')\n }, [hostId, formId, suppressNavigation])\n /** The read-only lockdown's own words (AGL-1511); never a hardcoded line. */\n const [pausedMessage, setPausedMessage] = useState('')\n /** The abuse ceiling's visitor notice (AGL-1666); see `unavailable` below. */\n const [unavailable, setUnavailable] =\n useState<Aglyn.FormUnavailableNotice | null>(null)\n const [alerts, setAlerts] = useState<\n Array<{ message: string; severity?: string }>\n >([])\n\n const handleSubmit = useCallback(\n async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault()\n if (!hostId || status === 'sending') return\n const data = new FormData(event.currentTarget)\n const fields: Record<string, string> = {}\n let website = ''\n for (const [key, value] of data.entries()) {\n if (typeof value !== 'string') continue\n if (key === 'website') {\n website = value\n continue\n }\n // `__`-prefixed inputs are internal controls — the rating field's\n // star radios (AGL-544) and each field's dataset-mapping input — and\n // never submit.\n if (key.startsWith('__')) continue\n // Checkbox groups emit one entry per ticked box; join them under\n // the field name instead of letting the last one win (AGL-544).\n fields[key] = (\n key in fields ? `${fields[key]}, ${value}` : value\n ).slice(0, 2000)\n }\n setStatus('sending')\n try {\n const response = await siteFetch('/api/forms/submit', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n // The bound form entity, when there is one. Sent ALONGSIDE the\n // caption rather than instead of it: the caption keeps the\n // pre-entity `?form=` filter working for every form that has not\n // been adopted yet, and no phase of the migration removes it.\n ...(formId ? { formId } : {}),\n formName: formName || 'Form',\n // The page's signed dataset binding (AGL-2773). The server writes\n // a record only where it says, so nothing this request names\n // chooses a dataset or a field.\n ...(datasetBindingToken\n ? { datasetBinding: datasetBindingToken }\n : {}),\n path: window.location.pathname,\n fields,\n website,\n // The campaign this visitor came from, when they came from one.\n // Absent entirely for direct traffic — never a placeholder, so\n // the server can tell \"arrived from nowhere\" from \"this door does\n // not report\".\n ...campaignTouchField(),\n }),\n })\n if (response.ok) {\n // Site alerts from the actions builder (AGL-148).\n const payload = await response.json().catch(() => ({}))\n if (Array.isArray(payload?.alerts)) setAlerts(payload.alerts)\n // Announced for reveal-style listeners/analytics (AGL-557).\n window.dispatchEvent(\n new CustomEvent(FORM_SUBMITTED_EVENT, {\n detail: { formName: formName || 'Form' },\n }),\n )\n // GA4 lead conversion (AGL-1561). Inside `response.ok`, so it\n // measures a SUBMISSION, not a click and not a validation failure —\n // the distinction that makes a conversion rate mean anything.\n //\n // AWAITED, because the branch below navigates. Ordering the call\n // ahead of the `assign` is not on its own enough, and the obvious\n // reasoning about beacons is the wrong one: once a hit REACHES gtag\n // a navigation cannot destroy it, so `transport_type: 'beacon'`\n // fixes nothing. What is lost is a hit that never reaches gtag,\n // which is what happens when the surface's transport is async and\n // its initialization promise is still pending — the continuation is\n // scheduled behind the navigation task and never runs. The tenant\n // runtime registers no transport, so this resolves immediately and\n // costs the redirect nothing; it is written this way because the\n // property that makes a bare call safe is invisible from here.\n //\n // `formName` is author-written site content, never a field VALUE:\n // no submitted field ever reaches GA from here.\n //\n // This runs on every tenant site, not only aglyn.com, and that is\n // deliberate — gtag is loaded with whatever measurement id the HOST\n // configured, so a customer's form reports into the customer's own\n // property and aglyn.com's reports into ours. It is consent-gated\n // either way: without a grant the script never loaded (AGL-1498)\n // and the delivery path finds no `window.gtag` to call.\n await trackEventBeforeNavigation('generate_lead', {\n form_name: formName || 'Form',\n form_location: window.location.pathname,\n })\n if (afterSubmit === 'redirect' && !suppressNavigation) {\n const target = resolveRedirectTarget(\n redirectScreenHref,\n redirectUrl,\n )\n // No valid target (deleted screen, rejected URL) degrades to\n // the success message below.\n if (target) formNavigation.assign(target)\n }\n }\n // A refusal is not a failure (AGL-1249). Preview declines writes on\n // purpose, and \"something went wrong — please try again\" invites the\n // author to retry something that is working exactly as intended.\n //\n // Since AGL-1511 there are TWO deliberate 423s a submit can meet, so\n // the status alone no longer identifies one: Preview's client-side\n // refusal, and a read-only lockdown refusing a real visitor's write\n // on a live site. Preview's carries `preview: true` and is settled\n // first; anything else 423 is the lockdown pause, whose own copy is\n // rendered rather than the Preview sentence — telling a customer's\n // visitor \"this works on your published site\" would be nonsense on\n // the published site they are standing on.\n if (!response.ok) {\n const body = await response.json().catch(() => ({}))\n if (Aglyn.isPreviewRefusal(response, body)) {\n setStatus('preview')\n return\n }\n const paused = Aglyn.parseLockdownRefusal(response.status, body)\n if (paused) {\n setPausedMessage(Aglyn.lockdownRefusalText(paused))\n setStatus('paused')\n return\n }\n // The site's form-submission abuse ceiling (AGL-1655/1666). This\n // is a THIRD deliberate refusal, and the only one whose status is\n // already spoken for: the Free plan's monthly wall answers 429\n // too. So it is matched on the body's `code` and never on the\n // status — `parseFormUnavailableRefusal` takes no status for\n // exactly that reason. A plan-wall 429 keeps falling through to\n // the generic branch below, which is right: it is the owner's\n // billing problem, and nothing about waiting fixes it.\n const notice = Aglyn.parseFormUnavailableRefusal(body)\n if (notice) {\n setUnavailable(notice)\n setStatus('unavailable')\n return\n }\n }\n setStatus(response.ok ? 'sent' : 'error')\n } catch {\n setStatus('error')\n }\n },\n [\n hostId,\n status,\n formId,\n formName,\n datasetBindingToken,\n afterSubmit,\n redirectScreenHref,\n redirectUrl,\n suppressNavigation,\n siteFetch,\n ],\n )\n\n if (status === 'sent') {\n // Reveal outcome: the form (with its hide-style below) unmounts and\n // the revealed element is the confirmation. Redirect keeps the\n // message visible as a fallback while the browser navigates.\n if (revealSelector) return null\n return (\n <Alert severity=\"success\">\n {successMessage || 'Thanks — your message has been sent.'}\n </Alert>\n )\n }\n\n return (\n <Stack\n ref={ref}\n component=\"form\"\n spacing={2}\n onSubmit={handleSubmit}\n onInput={handleFirstInput}\n {...rest}\n >\n {/* Hides the reveal target until submit (AGL-557); rendered by the\n form so SSR ships it hidden — no flash, no target-side props.\n MUI Stack spacing already skips <style> children. */}\n {revealSelector ? (\n <style>{`${revealSelector}{display:none !important}`}</style>\n ) : null}\n {children}\n {/* Honeypot: humans never see or fill this. */}\n <input\n type=\"text\"\n name=\"website\"\n tabIndex={-1}\n autoComplete=\"off\"\n aria-hidden=\"true\"\n style={{ position: 'absolute', left: '-5000px', height: 0, width: 0 }}\n />\n {status === 'error' ? (\n <Alert severity=\"error\">\n {'Something went wrong — please try again.'}\n </Alert>\n ) : null}\n {status === 'paused' ? (\n // `info`, like the Preview notice and for the same reason: this is a\n // deliberate, temporary pause on a site that is otherwise working,\n // and nothing the visitor typed was lost.\n <Alert severity=\"info\">{pausedMessage}</Alert>\n ) : null}\n {status === 'unavailable' && unavailable ? (\n // `warning`, not `info` and not `error` (AGL-1666). The two `info`\n // notices above are both cases where nothing was lost — Preview\n // never intended to send, and a lockdown pause is minutes long with\n // the visitor's text still in the fields. This one lost a real\n // message on a live site, so it must not read as reassurance. It is\n // not `error` either: nothing is broken and \"please try again\" would\n // walk the visitor into the same refusal.\n <Alert severity=\"warning\">\n {unavailable.message}\n {unavailable.contact ? (\n <>\n {' In the meantime you can reach us at '}\n <Link href={`mailto:${unavailable.contact}`}>\n {unavailable.contact}\n </Link>\n {'.'}\n </>\n ) : null}\n </Alert>\n ) : null}\n {status === 'preview' ? (\n // Deliberately `info`, not `error`, and it does not say \"try again\":\n // nothing failed, and the same submit works on the published site.\n <Alert severity=\"info\">\n {'Preview does not send form submissions. This form works on your ' +\n 'published site.'}\n </Alert>\n ) : null}\n {alerts.map((alert, index) => (\n <Alert key={index} severity={(alert.severity as any) || 'info'}>\n {alert.message}\n </Alert>\n ))}\n <Button\n type=\"submit\"\n variant=\"contained\"\n disabled={status === 'sending'}\n sx={{ alignSelf: 'flex-start' }}\n >\n {status === 'sending' ? 'Sending…' : submitLabel || 'Send'}\n </Button>\n </Stack>\n )\n})\nForm.displayName = 'AglynForm'\n\nexport interface FormFieldProps {\n /** Submission key; also the input's name attribute. */\n fieldName?: string\n /**\n * Stable model fieldId of the parent form's dataset this value is\n * stored under (AGL-556). When unset, values match dataset fields by\n * name — the legacy behavior.\n */\n datasetFieldId?: string\n label?: string\n /**\n * Grey hint shown inside the empty input (AGL-1330) — the frames pair a\n * label above the control with a placeholder inside it. A placeholder is\n * NOT a label: it disappears the moment the visitor types, so the label\n * stays the field's accessible name and is never replaced by this.\n * Ignored by radio, checkbox, and rating fields, which have no text input\n * to hint inside of.\n */\n placeholder?: string\n fieldType?:\n | 'text'\n | 'email'\n | 'textarea'\n | 'select'\n | 'radio'\n | 'checkbox'\n | 'rating'\n /**\n * Choice list for select/radio/checkbox fields (AGL-544): newline- or\n * comma-separated. Ignored by the other types.\n */\n options?: string\n required?: boolean\n}\n\n/**\n * Splits a newline- or comma-separated choice list into trimmed,\n * non-empty entries (AGL-544).\n */\nexport const parseFieldOptions = (options?: string): string[] =>\n (options || '')\n .split(/[\\n,]/)\n .map((entry) => entry.trim())\n .filter(Boolean)\n\n/**\n * Single input inside a Form (AGL-76). Survey field types — select,\n * radio, checkbox group, and star rating — are AGL-544.\n */\nconst FormField = forwardRef<HTMLDivElement, FormFieldProps>((props, ref) => {\n const {\n fieldName,\n datasetFieldId,\n label,\n placeholder,\n fieldType,\n options,\n required,\n ...rest\n } = props\n const name = fieldName || 'field'\n const fieldLabel = label || fieldName || 'Field'\n const choices = parseFieldOptions(options)\n // A CLEARED placeholder must render as no placeholder (AGL-1330). The\n // attributes form persists a cleared text field by dropping the key\n // (ddf maps an emptied field to its clearedValue and final-form's parse\n // turns `''` into `undefined`), and `updateNodeProps` REPLACES the props\n // object — so cleared reaches us as absent. Older documents can still\n // carry `''`/`null`, and a blank string would otherwise reach the DOM as\n // an empty `placeholder` attribute, so blank is normalized to absent too.\n const hint = typeof placeholder === 'string' && placeholder.trim()\n ? placeholder\n : undefined\n // MUI hides a placeholder for as long as the label still sits inside the\n // box, so the hint is only visible once the label is shrunk — and an\n // outlined input with a shrunk label must notch its outline or the label\n // crosses the border. Both are applied ONLY when there is a placeholder,\n // so fields without one keep the historical floating-label behavior.\n const hintSlotProps = hint\n ? { inputLabel: { shrink: true }, input: { notched: true } }\n : undefined\n // Checkbox groups count ticked boxes so \"required\" can mean \"at least\n // one\": native `required` is only asserted while none are ticked.\n const [checkedCount, setCheckedCount] = useState(0)\n // Rating is controlled so a hidden input can serialize the number.\n const [rating, setRating] = useState<number | null>(null)\n // Publishes this field's dataset schema-field mapping to the enclosing\n // Form via the DOM (AGL-556); `__`-prefixed, so never a submitted field.\n const mapInput = datasetFieldId ? (\n <input\n type=\"hidden\"\n name={`${FIELD_MAP_INPUT_PREFIX}${name}`}\n value={datasetFieldId}\n readOnly\n />\n ) : null\n\n if (fieldType === 'select') {\n return (\n <>\n <TextField\n ref={ref}\n select\n name={name}\n label={fieldLabel}\n required={Boolean(required)}\n defaultValue=\"\"\n fullWidth\n size=\"small\"\n // A Select has no native placeholder: the hint is what the closed\n // control displays while nothing is chosen, which needs\n // `displayEmpty` (MUI renders nothing for `''` without it).\n slotProps={\n hint\n ? {\n ...hintSlotProps,\n select: {\n displayEmpty: true,\n renderValue: (value: unknown) =>\n value ? (\n String(value)\n ) : (\n <span style={{ opacity: 0.6 }}>{hint}</span>\n ),\n },\n }\n : undefined\n }\n {...rest}\n >\n {choices.map((choice, index) => (\n <MenuItem key={index} value={choice}>\n {choice}\n </MenuItem>\n ))}\n </TextField>\n {mapInput}\n </>\n )\n }\n\n if (fieldType === 'radio') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <RadioGroup name={name}>\n {choices.map((choice, index) => (\n <FormControlLabel\n key={index}\n value={choice}\n label={choice}\n control={<Radio size=\"small\" required={Boolean(required)} />}\n />\n ))}\n </RadioGroup>\n {mapInput}\n </FormControl>\n )\n }\n\n if (fieldType === 'checkbox') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <FormGroup>\n {choices.map((choice, index) => (\n <FormControlLabel\n key={index}\n label={choice}\n control={\n <Checkbox\n name={name}\n value={choice}\n size=\"small\"\n required={Boolean(required) && checkedCount === 0}\n onChange={(event) =>\n setCheckedCount(\n (count) => count + (event.target.checked ? 1 : -1),\n )\n }\n />\n }\n />\n ))}\n </FormGroup>\n {mapInput}\n </FormControl>\n )\n }\n\n if (fieldType === 'rating') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <Rating\n // `__`-prefixed so the star radios themselves are skipped by\n // Form.handleSubmit; the hidden input carries the value.\n name={`__${name}`}\n value={rating}\n onChange={(_event, value) => setRating(value)}\n />\n <input type=\"hidden\" name={name} value={rating ?? ''} />\n {mapInput}\n </FormControl>\n )\n }\n\n return (\n <>\n <TextField\n ref={ref}\n name={name}\n label={fieldLabel}\n placeholder={hint}\n type={fieldType === 'email' ? 'email' : 'text'}\n required={Boolean(required)}\n multiline={fieldType === 'textarea'}\n minRows={fieldType === 'textarea' ? 3 : undefined}\n fullWidth\n size=\"small\"\n slotProps={hintSlotProps}\n {...rest}\n />\n {mapInput}\n </>\n )\n})\nFormField.displayName = 'AglynFormField'\n\nexport const formSchema: Aglyn.ComponentSchema<FormProps> = {\n $id: FORM_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Form',\n description:\n 'Collects its Form Fields and sends the answers to you, or into a dataset.',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiEmailFastOutline.path, sx: { color: 'info.main' } },\n attributes: [\n {\n name: 'formId',\n description:\n 'The form this collects for, from the Forms page. Stored by id, so ' +\n 'renaming the form never splits its submission history and two ' +\n 'pages placing the same form share one list. Leave unset to keep ' +\n 'filing submissions under the name below.',\n component: Aglyn.FieldComponentType.FORM_SELECT,\n label: 'Form',\n },\n {\n name: 'formName',\n description:\n 'Caption shown in the submissions inbox, used when no form above ' +\n 'is picked. A name is not an identity: renaming it splits the ' +\n 'history of everything already submitted under the old one.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Form name',\n },\n {\n name: 'datasetId',\n description:\n 'Optional dataset (from the Data page) submissions are appended ' +\n 'to. Stored by id, so renaming the dataset never breaks the ' +\n 'binding (AGL-556). The inbox always gets a copy.',\n component: Aglyn.FieldComponentType.DATASET_SELECT,\n label: 'Write to dataset',\n },\n {\n name: 'datasetName',\n description:\n 'Legacy name-based dataset binding — matched against the ' +\n \"dataset's display name at submit time. Pick the dataset above \" +\n 'and clear this; it only shows while a name is still set.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Write to dataset (legacy name)',\n condition: { when: 'datasetName', isNotEmpty: true },\n },\n {\n name: 'submitLabel',\n description: 'Label of the submit button.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Submit label',\n },\n {\n name: 'successMessage',\n description: 'Shown after a successful submission.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Success message',\n },\n // After-submit outcomes (AGL-557).\n {\n name: 'afterSubmit',\n description:\n 'What a successful submit does: show the success message, send ' +\n 'the visitor to another page, or reveal a hidden element.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'After submit',\n // `message` is the first member of the declared `FormAfterSubmit`\n // union, so the sentinel already existed in the type and only the\n // option list was spelling it `''` (AGL-1451) — a value the\n // attributes form strips on change (AGL-1191), which meant a form\n // switched to Redirect could not be switched back to its message.\n options: [\n { value: 'message', label: 'Show the success message' },\n { value: 'redirect', label: 'Redirect the visitor' },\n { value: 'reveal', label: 'Reveal a hidden element' },\n ],\n },\n {\n name: 'redirectScreenId',\n description:\n 'Screen the visitor lands on after submitting. The address is ' +\n \"resolved at render time, so renaming the screen's slug never \" +\n 'breaks the redirect.',\n component: Aglyn.FieldComponentType.SCREEN_SELECT,\n label: 'Redirect to screen',\n condition: { when: 'afterSubmit', is: 'redirect' },\n },\n {\n name: 'redirectUrl',\n description:\n 'Used only when no screen is selected above: a same-site path ' +\n '(/thanks) or an https URL. Anything else is ignored.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Redirect URL',\n condition: { when: 'afterSubmit', is: 'redirect' },\n },\n {\n name: 'revealNodeId',\n description:\n 'Element shown after a successful submit — it stays hidden on ' +\n 'the published page until then.',\n component: Aglyn.FieldComponentType.NODE_SELECT,\n label: 'Element to reveal',\n condition: { when: 'afterSubmit', is: 'reveal' },\n },\n ],\n}\n\nexport const formFieldSchema: Aglyn.ComponentSchema<FormFieldProps> = {\n $id: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Form Field',\n description:\n 'One input inside a form — text, select, radio, checkbox or rating.',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n flags: {\n selfClosing: Aglyn.FEATURE_FLAG.ENABLED,\n },\n attributes: [\n {\n name: 'fieldName',\n description: 'Key this value is stored under in submissions.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Field name',\n },\n {\n name: 'datasetFieldId',\n description:\n \"Schema field of the form's dataset this value is stored under. \" +\n 'Stored by id, so renaming the field never breaks the mapping ' +\n '(AGL-556). When unset, values match dataset fields by name.',\n component: Aglyn.FieldComponentType.DATASET_FIELD_SELECT,\n label: 'Maps to schema field',\n },\n {\n name: 'label',\n description: 'Visible input label.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Label',\n },\n {\n name: 'placeholder',\n description:\n 'Grey hint inside the empty input — \"you@company.com\" (AGL-1330). ' +\n 'It is not a label: it disappears as soon as the visitor types, so ' +\n 'always keep Label set. Ignored by radio, checkbox, and rating ' +\n 'fields. Clearing it removes the hint.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Placeholder',\n },\n {\n name: 'fieldType',\n description: 'Input type.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'Type',\n // `text` is the first member of this prop's own union, and the value\n // the render falls back to. As `''` it was the one field type an\n // author could not choose and keep (AGL-1191/AGL-1451): a field\n // switched to Email or Multiline could never be made plain text\n // again without deleting it.\n options: [\n { value: 'text', label: 'Text' },\n { value: 'email', label: 'Email' },\n { value: 'textarea', label: 'Multiline' },\n { value: 'select', label: 'Dropdown' },\n { value: 'radio', label: 'Radio choice' },\n { value: 'checkbox', label: 'Checkboxes' },\n { value: 'rating', label: 'Star rating' },\n ],\n },\n {\n name: 'options',\n description:\n 'Choices for dropdown, radio, and checkbox fields — one per line ' +\n '(or comma-separated). Ignored by the other types.',\n component: Aglyn.FieldComponentType.TEXTAREA,\n label: 'Options',\n },\n {\n name: 'required',\n description: 'Whether the field must be filled.',\n component: Aglyn.FieldComponentType.SWITCH,\n label: 'Required?',\n },\n ],\n}\n\nexport const formPresets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(FORM_ID, 'contact'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Contact Form',\n pluginId: BUNDLE_ID,\n description: 'Name, email, and message with a send button',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiEmailFastOutline.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_ID,\n pluginId: BUNDLE_ID,\n props: { formName: 'Contact' },\n nodes: [\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName: 'name', label: 'Name', required: true },\n },\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: {\n fieldName: 'email',\n label: 'Email',\n fieldType: 'email',\n required: true,\n },\n },\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: {\n fieldName: 'message',\n label: 'Message',\n fieldType: 'textarea',\n required: true,\n },\n },\n ],\n },\n },\n {\n $id: generatePresetId(FORM_FIELD_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Form Field',\n pluginId: BUNDLE_ID,\n description: 'Single input inside a form',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName: 'field', label: 'Field' },\n },\n },\n /*\n * The marketing consent field as one preset rather than a recipe. Its props\n * are the platform's `MARKETING_CONSENT_FORM_FIELD`, which every place that adds\n * this field reads, so each adds the same unticked one-option checkbox. It\n * records consent once the form names it as its Marketing consent field.\n */\n {\n $id: generatePresetId(FORM_FIELD_ID, 'marketing-consent'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Marketing consent',\n pluginId: BUNDLE_ID,\n description: 'An unticked checkbox asking permission to send marketing emails',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { ...Aglyn.MARKETING_CONSENT_FORM_FIELD },\n },\n },\n]\n\n/**\n * The composed section a form usually arrives inside.\n *\n * It lives here rather than with the generic block library because it PLACES\n * form nodes, and a preset's `data` carries the `pluginId` those nodes are\n * saved with — so authoring it anywhere else stamps every contact form on\n * every new site with the wrong bundle. The heading and the stack around it\n * name `mui`, which is correct: they ARE mui elements, and a page holding this\n * section needs both bundles registered before it paints.\n */\nexport const formBlockPresets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(FORM_ID, 'contact-section'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Contact Section',\n pluginId: BUNDLE_ID,\n description: 'Heading with a name/email/message form',\n category: Aglyn.ComponentCategory.BLOCKS,\n icon: { path: mdiEmailOutline.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: 'muiStack',\n pluginId: MUI_BUNDLE_ID,\n props: { spacing: 2 },\n sx: {\n paddingLeft: 4,\n paddingRight: 4,\n paddingTop: 6,\n paddingBottom: 6,\n maxWidth: 560,\n },\n nodes: [\n {\n $id: null,\n componentId: 'muiTypography',\n pluginId: MUI_BUNDLE_ID,\n props: { variant: 'h4', children: 'Get in touch' },\n },\n {\n $id: null,\n componentId: FORM_ID,\n pluginId: BUNDLE_ID,\n props: {\n formName: 'Contact',\n submitLabel: 'Send message',\n successMessage: 'Thanks — we will get back to you soon.',\n },\n nodes: [\n contactField('name', 'Name'),\n contactField('email', 'Email', { fieldType: 'email' }),\n contactField('message', 'Message', { fieldType: 'textarea' }),\n ],\n },\n ],\n },\n },\n]\n\nfunction contactField(fieldName: string, label: string, extra?: object) {\n return {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName, label, required: true, ...extra },\n }\n}\n\nexport { Form, FormField }\nexport default Form\n"],"names":["sendAnalyticsBeacon","trackEventBeforeNavigation","campaignTouchField","Aglyn","mdiEmailFastOutline","mdiEmailOutline","mdiFormTextbox","isSameOriginPath","Alert","Button","Checkbox","FormControl","FormControlLabel","FormGroup","FormLabel","Link","MenuItem","Radio","RadioGroup","Rating","Stack","TextField","forwardRef","useCallback","useEffect","useRef","useState","BUNDLE_ID","generatePresetId","MUI_BUNDLE_ID","FORM_ID","FORM_FIELD_ID","FIELD_MAP_INPUT_PREFIX","sanitizeRedirectUrl","url","trimmed","trim","undefined","test","resolveRedirectTarget","screenHref","redirectUrl","REVEAL_NODE_ID_PATTERN","formNavigation","assign","window","location","FORM_SUBMITTED_EVENT","sendFormBeacon","hostId","formId","event","form","Form","props","ref","formName","datasetId","_datasetId","datasetName","_datasetName","datasetBindingToken","submitLabel","successMessage","afterSubmit","redirectScreenId","revealNodeId","children","rest","useSite","siteFetch","useSiteFetch","href","redirectScreenHref","suppressNavigation","useScreenLink","revealSelector","status","setStatus","reported","view","start","current","handleFirstInput","pausedMessage","setPausedMessage","unavailable","setUnavailable","alerts","setAlerts","handleSubmit","preventDefault","data","FormData","currentTarget","fields","website","key","value","entries","startsWith","slice","response","method","headers","body","JSON","stringify","datasetBinding","path","pathname","ok","payload","json","catch","Array","isArray","dispatchEvent","CustomEvent","detail","form_name","form_location","target","isPreviewRefusal","paused","parseLockdownRefusal","lockdownRefusalText","notice","parseFormUnavailableRefusal","severity","component","spacing","onSubmit","onInput","style","input","type","name","tabIndex","autoComplete","aria-hidden","position","left","height","width","message","contact","map","alert","index","variant","disabled","sx","alignSelf","displayName","parseFieldOptions","options","split","entry","filter","Boolean","FormField","fieldName","datasetFieldId","label","placeholder","fieldType","required","fieldLabel","choices","hint","hintSlotProps","inputLabel","shrink","notched","checkedCount","setCheckedCount","rating","setRating","mapInput","readOnly","select","defaultValue","fullWidth","size","slotProps","displayEmpty","renderValue","String","span","opacity","choice","control","onChange","count","checked","_event","multiline","minRows","formSchema","$id","pluginId","description","category","ComponentCategory","FORMS","icon","color","attributes","FieldComponentType","FORM_SELECT","TEXT_FIELD","DATASET_SELECT","condition","when","isNotEmpty","SELECT","SCREEN_SELECT","is","NODE_SELECT","formFieldSchema","flags","selfClosing","FEATURE_FLAG","ENABLED","DATASET_FIELD_SELECT","TEXTAREA","SWITCH","formPresets","NodeType","PRESET","componentId","nodes","MARKETING_CONSENT_FORM_FIELD","formBlockPresets","BLOCKS","paddingLeft","paddingRight","paddingTop","paddingBottom","maxWidth","contactField","extra"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { sendAnalyticsBeacon } from '@aglyn/aglyn/app-utils/analytics-beacon'\nimport { trackEventBeforeNavigation } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { campaignTouchField } from '@aglyn/aglyn/app-utils/campaign-touch'\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n mdiEmailFastOutline,\n mdiEmailOutline,\n mdiFormTextbox,\n} from '@aglyn/shared-data-mdi'\nimport { isSameOriginPath } from '@aglyn/shared-util-http/safe-redirect'\nimport Alert from '@mui/material/Alert'\nimport Button from '@mui/material/Button'\nimport Checkbox from '@mui/material/Checkbox'\nimport FormControl from '@mui/material/FormControl'\nimport FormControlLabel from '@mui/material/FormControlLabel'\nimport FormGroup from '@mui/material/FormGroup'\nimport FormHelperText from '@mui/material/FormHelperText'\nimport FormLabel from '@mui/material/FormLabel'\nimport Link from '@mui/material/Link'\nimport MenuItem from '@mui/material/MenuItem'\nimport Radio from '@mui/material/Radio'\nimport RadioGroup from '@mui/material/RadioGroup'\nimport Rating from '@mui/material/Rating'\nimport Stack from '@mui/material/Stack'\nimport TextField from '@mui/material/TextField'\nimport {\n createContext,\n type FormEvent,\n forwardRef,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n} from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n/*\n * The heading and the stack this block's form sits inside are mui elements,\n * so the nodes that carry them say so. A node's `pluginId` names the bundle\n * that REGISTERS its component, not the bundle whose preset placed it.\n *\n * The id comes from the core's constant, as it does for every plugin that\n * composes mui elements: importing the mui plugin for one string would make\n * this plugin unusable without it on the import graph.\n */\nconst { MUI_BUNDLE_ID } = Aglyn\n\n// Component ids are persisted in screen documents; never rename.\nexport const FORM_ID: Aglyn.ComponentId = 'form'\nexport const FORM_FIELD_ID: Aglyn.ComponentId = 'formField'\n\n/** After-submit outcomes (AGL-557); message is the historical default. */\nexport type FormAfterSubmit = 'message' | 'redirect' | 'reveal'\n\nexport interface FormProps {\n /**\n * The form entity this node submits as (`docs/specs/reusable-forms.md` §2c).\n *\n * THE identity. `formName` below is a caption and always was: it is copied\n * onto each submission at write time and reconciled with nothing, so\n * renaming a form split its history in two and two pages sharing a label\n * were one list. An id survives both.\n *\n * This is the whole binding. Reuse across pages needs nothing further —\n * promote the bound subtree once and the id travels inside the definition,\n * so every instance writes the same form's submissions without the author\n * retyping a label. Two instances of one definition are ONE form, which is\n * the point; an author who wants two forms makes two forms.\n */\n formId?: string\n /** Identifies the form in the submissions inbox. */\n formName?: string\n /**\n * Dataset (by id) submissions also append into (AGL-556); the inbox\n * copy is always written. Renaming the dataset never breaks the binding.\n */\n datasetId?: string\n /**\n * Legacy dataset-by-name binding (AGL-141), honored when no `datasetId`\n * is set; kept so persisted nodes keep working.\n */\n datasetName?: string\n /**\n * The dataset binding above, signed by the compose that rendered this form\n * (AGL-2773). The submit route writes a record only where this says, so the\n * page, not the request, decides which dataset a submission reaches. Present\n * on the published site only; the canvas and Preview never submit.\n */\n datasetBindingToken?: string\n submitLabel?: string\n successMessage?: string\n /**\n * What a successful submit does (AGL-557): show the success message\n * (default), redirect the visitor, or reveal a hidden element.\n */\n afterSubmit?: FormAfterSubmit\n /** Redirect target screen — rename-safe id, resolved like ScreenLink. */\n redirectScreenId?: string\n /**\n * Manual redirect target used when no screen is picked: a same-site\n * `/path` or an absolute https URL (everything else is dropped).\n */\n redirectUrl?: string\n /** Node revealed on submit; hidden until then (afterSubmit=reveal). */\n revealNodeId?: string\n children?: React.ReactNode\n}\n\n/**\n * Hidden-input prefix a FormField uses to publish its field → dataset\n * schema-field mapping to the enclosing Form (AGL-556). Rides the DOM so\n * arbitrary nesting between Form and field needs no React context; the\n * `__` prefix keeps it out of the submitted fields.\n */\nexport const FIELD_MAP_INPUT_PREFIX = '__map__'\n\n/**\n * The hidden input the field carrying the consent-group sentence posts its\n * key under (AGL-3320). `__`-prefixed like the map inputs, so it never\n * reaches the submitted fields; the Form lifts it into the request body as\n * `__consentGroup`, where the submit route reads it.\n *\n * Posted by the FIELD, not the Form: the key is proof the sentence was on\n * screen, and the field that rendered it is the only thing that knows it was.\n * A form whose consent field never mounted posts no key, and its opt-in is\n * recorded for the one site the visitor is on.\n */\nexport const CONSENT_GROUP_INPUT = '__consentGroup'\n\n/** The site's consent-group sentence, as `/api/consent/disclosure` answers it. */\nexport interface FormConsentDisclosure {\n /**\n * The field the form declares as its opt-in, or `null` for a form that\n * declares none — the sentence then sits under whichever field the submit\n * route would read an opt-in from by its name.\n */\n fieldName: string | null\n /** What the person is told they are agreeing to hear from. */\n text: string\n /** The key of exactly this sentence, posted back with the submission. */\n key: string\n}\n\n/**\n * The disclosure a Form fetched, handed to the fields inside it. A context\n * because the answer flows DOWN, from the one request the Form makes to the\n * one field it describes; the key flows back up through the DOM, like the\n * dataset mapping does.\n */\nconst FormConsentDisclosureContext =\n createContext<FormConsentDisclosure | null>(null)\n\n/**\n * The route's answer, or `null` when it has no sentence for this site — a\n * site that sends on its own, or an answer that is not the shape it should\n * be. `null` renders nothing and posts no key.\n */\nexport function readFormConsentDisclosure(\n body: unknown,\n): FormConsentDisclosure | null {\n const value = (body ?? {}) as Record<string, unknown>\n const text = typeof value['text'] === 'string' ? value['text'].trim() : ''\n const key = typeof value['key'] === 'string' ? value['key'].trim() : ''\n if (!text || !key) return null\n const fieldName =\n typeof value['fieldName'] === 'string' && value['fieldName'].trim()\n ? value['fieldName'].trim()\n : null\n return { fieldName, text, key }\n}\n\n/**\n * Redirect targets are restricted to same-origin paths (`/thanks`) and\n * absolute https URLs (AGL-557): stored props reach every visitor's\n * browser, so `javascript:`/`data:` URLs — and protocol-relative\n * `//host` forms, which keep the scheme but swap the host — never pass.\n *\n * The relative branch asks {@link isSameOriginPath} rather than reading the\n * string's first two characters (AGL-1881). This site never got the AGL-2486\n * backslash fix, and the character list would not have been enough anyway:\n * the URL parser deletes tab/LF/CR before parsing, so `/<TAB>/evil.com`\n * carries neither `//` nor `\\` and `window.location.assign` — which\n * `formNavigation` below calls — still lands the visitor on `evil.com`. The\n * absolute branch is unchanged: an explicit `https://` target is a documented\n * feature, and for absolute input the parser's verdict was never in doubt.\n */\nexport const sanitizeRedirectUrl = (url?: string): string | undefined => {\n const trimmed = (url ?? '').trim()\n if (!trimmed) return undefined\n if (/^https:\\/\\//i.test(trimmed)) return trimmed\n if (isSameOriginPath(trimmed)) return trimmed\n return undefined\n}\n\n/**\n * Where a redirect outcome sends the visitor: the picked screen's\n * resolved href wins; the manual URL is the fallback, sanitized above.\n * Undefined (deleted screen, bad URL) degrades to the message outcome.\n */\nexport const resolveRedirectTarget = (\n screenHref: string | undefined,\n redirectUrl: string | undefined,\n): string | undefined => screenHref ?? sanitizeRedirectUrl(redirectUrl)\n\n// Reveal ids are interpolated into a CSS selector — restrict to the id\n// alphabet so a stored prop can't smuggle selector syntax (AGL-557).\nconst REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/\n\n/**\n * Navigation seam for the redirect outcome: jsdom's `window.location`\n * is not patchable, so tests stub this indirection instead.\n */\nexport const formNavigation = {\n assign: (url: string) => window.location.assign(url),\n}\n\n/** DOM event dispatched on every successful submit (AGL-557). */\nexport const FORM_SUBMITTED_EVENT = 'aglyn:form-submitted'\n\n/**\n * Report that a bound form was seen, or typed into.\n *\n * Fire-and-forget, and deliberately the same instrument the overlay counters\n * use: `sendBeacon` hands the request to the browser, which delivers it\n * outside the page's lifetime, so nothing here can delay a render or survive\n * long enough to fail visibly.\n *\n * Requires a bound `formId`. An unbound form has no document to count on, and\n * an id is never invented: the collector's `update` would find nothing, which\n * is the right outcome but a pointless request.\n */\nexport function sendFormBeacon(\n hostId: string | undefined,\n formId: string | undefined,\n event: 'view' | 'start',\n): void {\n if (!hostId || !formId) return\n // Best-effort by construction: `sendAnalyticsBeacon` swallows everything and\n // refuses outright from a non-production build or a browser marked ours. A\n // view that does not report is a view this form's completion rate is\n // measured without, which the console says.\n sendAnalyticsBeacon({ hostId, formId, form: event })\n}\n\n/**\n * Lead-capture form (AGL-76): collects its field children's values and\n * posts them to the tenant's `/api/forms/submit` with the site's host id\n * from SiteContext. Without a site context (besigner canvas, preview) the\n * submit is inert, so editing never creates submissions. Includes a\n * honeypot input for naive bots.\n */\nconst Form = forwardRef<HTMLFormElement, FormProps>((props, ref) => {\n const {\n formId,\n formName,\n // Read by compose, which signs them into `datasetBindingToken`; taken out\n // here only so they never reach the DOM through `rest`.\n datasetId: _datasetId,\n datasetName: _datasetName,\n datasetBindingToken,\n submitLabel,\n successMessage,\n afterSubmit,\n redirectScreenId,\n redirectUrl,\n revealNodeId,\n children,\n ...rest\n } = props\n const { hostId } = Aglyn.useSite()\n const siteFetch = Aglyn.useSiteFetch()\n // Resolves the redirect screen like ScreenLink does (rename-safe) and\n // flags editing surfaces, where outcomes must never fire (AGL-557).\n const { href: redirectScreenHref, suppressNavigation } = Aglyn.useScreenLink(\n afterSubmit === 'redirect' ? redirectScreenId : undefined,\n )\n // Reveal target (AGL-557): hidden through a form-owned <style> tag so\n // SSR paints it hidden from the first frame; the style unmounts with\n // the form after a successful submit — which IS the reveal. Editing\n // surfaces keep the target visible so it stays editable.\n const revealSelector =\n afterSubmit === 'reveal' &&\n revealNodeId &&\n REVEAL_NODE_ID_PATTERN.test(revealNodeId) &&\n !suppressNavigation\n ? `[data-aglyn=\"leaf:${revealNodeId}\"]`\n : undefined\n const [status, setStatus] = useState<\n 'idle' | 'sending' | 'sent' | 'error' | 'preview' | 'paused' | 'unavailable'\n >(\n 'idle',\n )\n /*\n * THE VIEW AND THE START — the two facts a completion rate is taken over.\n *\n * Both are reported ONCE per mounted form, from a ref rather than from\n * state: a counter driven by a re-render would be a Firestore write per\n * keystroke, which is the per-render write this must not become. The ref\n * also survives the re-renders `status` causes, so a visitor who edits a\n * field, submits, and is shown an error is one view and one start.\n *\n * Gated on `!suppressNavigation`, the editing-surface flag: the besigner\n * canvas and the console's preview render this component for its author,\n * and counting an author looking at their own draft as a visitor view would\n * put the merchant into their own denominator.\n */\n const reported = useRef({ view: false, start: false })\n useEffect(() => {\n if (suppressNavigation || reported.current.view) return\n reported.current.view = true\n sendFormBeacon(hostId, formId, 'view')\n }, [hostId, formId, suppressNavigation])\n\n /**\n * A START is the first edit anyone makes to this form.\n *\n * On the form's own `input`, which bubbles from every field — no listener\n * registration, no per-field wiring, and nothing to keep in sync when a\n * field type is added. Focus would be the looser reading and the wrong one:\n * a form that is tabbed through and abandoned was never started, and\n * counting it would inflate the denominator abandonment is measured over.\n */\n const handleFirstInput = useCallback(() => {\n if (suppressNavigation || reported.current.start) return\n reported.current.start = true\n sendFormBeacon(hostId, formId, 'start')\n }, [hostId, formId, suppressNavigation])\n /** The read-only lockdown's own words (AGL-1511); never a hardcoded line. */\n const [pausedMessage, setPausedMessage] = useState('')\n /** The abuse ceiling's visitor notice (AGL-1666); see `unavailable` below. */\n const [unavailable, setUnavailable] =\n useState<Aglyn.FormUnavailableNotice | null>(null)\n const [alerts, setAlerts] = useState<\n Array<{ message: string; severity?: string }>\n >([])\n /*\n * THE CONSENT-GROUP SENTENCE, when this site sends as part of a group\n * (AGL-3320).\n *\n * Asked for on the rendered site only — the canvas has no site to ask\n * about — and handed to the fields, one of which renders it under the\n * opt-in and posts its key. Until the answer arrives, and whenever it has\n * none, nothing renders and nothing is posted: the opt-in is then recorded\n * for this site alone, which is the safe way to be early or wrong.\n */\n const [disclosure, setDisclosure] = useState<FormConsentDisclosure | null>(\n null,\n )\n useEffect(() => {\n if (!hostId) return\n let current = true\n const query = new URLSearchParams({ hostId, ...(formId ? { formId } : {}) })\n siteFetch(`/api/consent/disclosure?${query.toString()}`)\n .then((response) => (response.ok ? response.json() : null))\n .then((body) => {\n if (current) setDisclosure(readFormConsentDisclosure(body))\n })\n .catch(() => {\n if (current) setDisclosure(null)\n })\n return () => {\n current = false\n }\n }, [hostId, formId, siteFetch])\n\n const handleSubmit = useCallback(\n async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault()\n if (!hostId || status === 'sending') return\n const data = new FormData(event.currentTarget)\n const fields: Record<string, string> = {}\n let website = ''\n let consentGroup = ''\n for (const [key, value] of data.entries()) {\n if (typeof value !== 'string') continue\n if (key === 'website') {\n website = value\n continue\n }\n // The key the consent field posted beside the sentence it rendered.\n if (key === CONSENT_GROUP_INPUT) {\n consentGroup = value\n continue\n }\n // `__`-prefixed inputs are internal controls — the rating field's\n // star radios (AGL-544) and each field's dataset-mapping input — and\n // never submit.\n if (key.startsWith('__')) continue\n // Checkbox groups emit one entry per ticked box; join them under\n // the field name instead of letting the last one win (AGL-544).\n fields[key] = (\n key in fields ? `${fields[key]}, ${value}` : value\n ).slice(0, 2000)\n }\n setStatus('sending')\n try {\n const response = await siteFetch('/api/forms/submit', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n // The bound form entity, when there is one. Sent ALONGSIDE the\n // caption rather than instead of it: the caption keeps the\n // pre-entity `?form=` filter working for every form that has not\n // been adopted yet, and no phase of the migration removes it.\n ...(formId ? { formId } : {}),\n formName: formName || 'Form',\n // The page's signed dataset binding (AGL-2773). The server writes\n // a record only where it says, so nothing this request names\n // chooses a dataset or a field.\n ...(datasetBindingToken\n ? { datasetBinding: datasetBindingToken }\n : {}),\n path: window.location.pathname,\n fields,\n website,\n // Only when the consent field rendered the site's consent-group\n // sentence; the route pools the opt-in on nothing else.\n ...(consentGroup ? { [CONSENT_GROUP_INPUT]: consentGroup } : {}),\n // The campaign this visitor came from, when they came from one.\n // Absent entirely for direct traffic — never a placeholder, so\n // the server can tell \"arrived from nowhere\" from \"this door does\n // not report\".\n ...campaignTouchField(),\n }),\n })\n if (response.ok) {\n // Site alerts from the actions builder (AGL-148).\n const payload = await response.json().catch(() => ({}))\n if (Array.isArray(payload?.alerts)) setAlerts(payload.alerts)\n // Announced for reveal-style listeners/analytics (AGL-557).\n window.dispatchEvent(\n new CustomEvent(FORM_SUBMITTED_EVENT, {\n detail: { formName: formName || 'Form' },\n }),\n )\n // GA4 lead conversion (AGL-1561). Inside `response.ok`, so it\n // measures a SUBMISSION, not a click and not a validation failure —\n // the distinction that makes a conversion rate mean anything.\n //\n // AWAITED, because the branch below navigates. Ordering the call\n // ahead of the `assign` is not on its own enough, and the obvious\n // reasoning about beacons is the wrong one: once a hit REACHES gtag\n // a navigation cannot destroy it, so `transport_type: 'beacon'`\n // fixes nothing. What is lost is a hit that never reaches gtag,\n // which is what happens when the surface's transport is async and\n // its initialization promise is still pending — the continuation is\n // scheduled behind the navigation task and never runs. The tenant\n // runtime registers no transport, so this resolves immediately and\n // costs the redirect nothing; it is written this way because the\n // property that makes a bare call safe is invisible from here.\n //\n // `formName` is author-written site content, never a field VALUE:\n // no submitted field ever reaches GA from here.\n //\n // This runs on every tenant site, not only aglyn.com, and that is\n // deliberate — gtag is loaded with whatever measurement id the HOST\n // configured, so a customer's form reports into the customer's own\n // property and aglyn.com's reports into ours. It is consent-gated\n // either way: without a grant the script never loaded (AGL-1498)\n // and the delivery path finds no `window.gtag` to call.\n await trackEventBeforeNavigation('generate_lead', {\n form_name: formName || 'Form',\n form_location: window.location.pathname,\n })\n if (afterSubmit === 'redirect' && !suppressNavigation) {\n const target = resolveRedirectTarget(\n redirectScreenHref,\n redirectUrl,\n )\n // No valid target (deleted screen, rejected URL) degrades to\n // the success message below.\n if (target) formNavigation.assign(target)\n }\n }\n // A refusal is not a failure (AGL-1249). Preview declines writes on\n // purpose, and \"something went wrong — please try again\" invites the\n // author to retry something that is working exactly as intended.\n //\n // Since AGL-1511 there are TWO deliberate 423s a submit can meet, so\n // the status alone no longer identifies one: Preview's client-side\n // refusal, and a read-only lockdown refusing a real visitor's write\n // on a live site. Preview's carries `preview: true` and is settled\n // first; anything else 423 is the lockdown pause, whose own copy is\n // rendered rather than the Preview sentence — telling a customer's\n // visitor \"this works on your published site\" would be nonsense on\n // the published site they are standing on.\n if (!response.ok) {\n const body = await response.json().catch(() => ({}))\n if (Aglyn.isPreviewRefusal(response, body)) {\n setStatus('preview')\n return\n }\n const paused = Aglyn.parseLockdownRefusal(response.status, body)\n if (paused) {\n setPausedMessage(Aglyn.lockdownRefusalText(paused))\n setStatus('paused')\n return\n }\n // The site's form-submission abuse ceiling (AGL-1655/1666). This\n // is a THIRD deliberate refusal, and the only one whose status is\n // already spoken for: the Free plan's monthly wall answers 429\n // too. So it is matched on the body's `code` and never on the\n // status — `parseFormUnavailableRefusal` takes no status for\n // exactly that reason. A plan-wall 429 keeps falling through to\n // the generic branch below, which is right: it is the owner's\n // billing problem, and nothing about waiting fixes it.\n const notice = Aglyn.parseFormUnavailableRefusal(body)\n if (notice) {\n setUnavailable(notice)\n setStatus('unavailable')\n return\n }\n }\n setStatus(response.ok ? 'sent' : 'error')\n } catch {\n setStatus('error')\n }\n },\n [\n hostId,\n status,\n formId,\n formName,\n datasetBindingToken,\n afterSubmit,\n redirectScreenHref,\n redirectUrl,\n suppressNavigation,\n siteFetch,\n ],\n )\n\n if (status === 'sent') {\n // Reveal outcome: the form (with its hide-style below) unmounts and\n // the revealed element is the confirmation. Redirect keeps the\n // message visible as a fallback while the browser navigates.\n if (revealSelector) return null\n return (\n <Alert severity=\"success\">\n {successMessage || 'Thanks — your message has been sent.'}\n </Alert>\n )\n }\n\n return (\n <Stack\n ref={ref}\n component=\"form\"\n spacing={2}\n onSubmit={handleSubmit}\n onInput={handleFirstInput}\n {...rest}\n >\n {/* Hides the reveal target until submit (AGL-557); rendered by the\n form so SSR ships it hidden — no flash, no target-side props.\n MUI Stack spacing already skips <style> children. */}\n {revealSelector ? (\n <style>{`${revealSelector}{display:none !important}`}</style>\n ) : null}\n <FormConsentDisclosureContext.Provider value={disclosure}>\n {children}\n </FormConsentDisclosureContext.Provider>\n {/* Honeypot: humans never see or fill this. */}\n <input\n type=\"text\"\n name=\"website\"\n tabIndex={-1}\n autoComplete=\"off\"\n aria-hidden=\"true\"\n style={{ position: 'absolute', left: '-5000px', height: 0, width: 0 }}\n />\n {status === 'error' ? (\n <Alert severity=\"error\">\n {'Something went wrong — please try again.'}\n </Alert>\n ) : null}\n {status === 'paused' ? (\n // `info`, like the Preview notice and for the same reason: this is a\n // deliberate, temporary pause on a site that is otherwise working,\n // and nothing the visitor typed was lost.\n <Alert severity=\"info\">{pausedMessage}</Alert>\n ) : null}\n {status === 'unavailable' && unavailable ? (\n // `warning`, not `info` and not `error` (AGL-1666). The two `info`\n // notices above are both cases where nothing was lost — Preview\n // never intended to send, and a lockdown pause is minutes long with\n // the visitor's text still in the fields. This one lost a real\n // message on a live site, so it must not read as reassurance. It is\n // not `error` either: nothing is broken and \"please try again\" would\n // walk the visitor into the same refusal.\n <Alert severity=\"warning\">\n {unavailable.message}\n {unavailable.contact ? (\n <>\n {' In the meantime you can reach us at '}\n <Link href={`mailto:${unavailable.contact}`}>\n {unavailable.contact}\n </Link>\n {'.'}\n </>\n ) : null}\n </Alert>\n ) : null}\n {status === 'preview' ? (\n // Deliberately `info`, not `error`, and it does not say \"try again\":\n // nothing failed, and the same submit works on the published site.\n <Alert severity=\"info\">\n {'Preview does not send form submissions. This form works on your ' +\n 'published site.'}\n </Alert>\n ) : null}\n {alerts.map((alert, index) => (\n <Alert key={index} severity={(alert.severity as any) || 'info'}>\n {alert.message}\n </Alert>\n ))}\n <Button\n type=\"submit\"\n variant=\"contained\"\n disabled={status === 'sending'}\n sx={{ alignSelf: 'flex-start' }}\n >\n {status === 'sending' ? 'Sending…' : submitLabel || 'Send'}\n </Button>\n </Stack>\n )\n})\nForm.displayName = 'AglynForm'\n\nexport interface FormFieldProps {\n /** Submission key; also the input's name attribute. */\n fieldName?: string\n /**\n * Stable model fieldId of the parent form's dataset this value is\n * stored under (AGL-556). When unset, values match dataset fields by\n * name — the legacy behavior.\n */\n datasetFieldId?: string\n label?: string\n /**\n * Grey hint shown inside the empty input (AGL-1330) — the frames pair a\n * label above the control with a placeholder inside it. A placeholder is\n * NOT a label: it disappears the moment the visitor types, so the label\n * stays the field's accessible name and is never replaced by this.\n * Ignored by radio, checkbox, and rating fields, which have no text input\n * to hint inside of.\n */\n placeholder?: string\n fieldType?:\n | 'text'\n | 'email'\n | 'textarea'\n | 'select'\n | 'radio'\n | 'checkbox'\n | 'rating'\n /**\n * Choice list for select/radio/checkbox fields (AGL-544): newline- or\n * comma-separated. Ignored by the other types.\n */\n options?: string\n required?: boolean\n}\n\n/**\n * Splits a newline- or comma-separated choice list into trimmed,\n * non-empty entries (AGL-544).\n */\nexport const parseFieldOptions = (options?: string): string[] =>\n (options || '')\n .split(/[\\n,]/)\n .map((entry) => entry.trim())\n .filter(Boolean)\n\n/**\n * Single input inside a Form (AGL-76). Survey field types — select,\n * radio, checkbox group, and star rating — are AGL-544.\n */\nconst FormField = forwardRef<HTMLDivElement, FormFieldProps>((props, ref) => {\n const {\n fieldName,\n datasetFieldId,\n label,\n placeholder,\n fieldType,\n options,\n required,\n ...rest\n } = props\n const name = fieldName || 'field'\n const fieldLabel = label || fieldName || 'Field'\n const choices = parseFieldOptions(options)\n // A CLEARED placeholder must render as no placeholder (AGL-1330). The\n // attributes form persists a cleared text field by dropping the key\n // (ddf maps an emptied field to its clearedValue and final-form's parse\n // turns `''` into `undefined`), and `updateNodeProps` REPLACES the props\n // object — so cleared reaches us as absent. Older documents can still\n // carry `''`/`null`, and a blank string would otherwise reach the DOM as\n // an empty `placeholder` attribute, so blank is normalized to absent too.\n const hint = typeof placeholder === 'string' && placeholder.trim()\n ? placeholder\n : undefined\n // MUI hides a placeholder for as long as the label still sits inside the\n // box, so the hint is only visible once the label is shrunk — and an\n // outlined input with a shrunk label must notch its outline or the label\n // crosses the border. Both are applied ONLY when there is a placeholder,\n // so fields without one keep the historical floating-label behavior.\n const hintSlotProps = hint\n ? { inputLabel: { shrink: true }, input: { notched: true } }\n : undefined\n // Checkbox groups count ticked boxes so \"required\" can mean \"at least\n // one\": native `required` is only asserted while none are ticked.\n const [checkedCount, setCheckedCount] = useState(0)\n // Rating is controlled so a hidden input can serialize the number.\n const [rating, setRating] = useState<number | null>(null)\n // Publishes this field's dataset schema-field mapping to the enclosing\n // Form via the DOM (AGL-556); `__`-prefixed, so never a submitted field.\n const mapInput = datasetFieldId ? (\n <input\n type=\"hidden\"\n name={`${FIELD_MAP_INPUT_PREFIX}${name}`}\n value={datasetFieldId}\n readOnly\n />\n ) : null\n /*\n * THE CONSENT-GROUP SENTENCE, under the field that records the opt-in\n * (AGL-3320): the field the form declares, or — where it declares none —\n * one the submit route reads an opt-in from by its name. The key rides\n * beside it, so a submission carries the key only while the sentence is\n * on screen.\n */\n const disclosure = useContext(FormConsentDisclosureContext)\n const describesOptIn = disclosure\n ? disclosure.fieldName\n ? disclosure.fieldName === name\n : Aglyn.isMarketingConsentFieldName(name)\n : false\n const consentNote =\n disclosure && describesOptIn ? (\n <>\n <FormHelperText>{disclosure.text}</FormHelperText>\n <input\n type=\"hidden\"\n name={CONSENT_GROUP_INPUT}\n value={disclosure.key}\n readOnly\n />\n </>\n ) : null\n\n if (fieldType === 'select') {\n return (\n <>\n <TextField\n ref={ref}\n select\n name={name}\n label={fieldLabel}\n required={Boolean(required)}\n defaultValue=\"\"\n fullWidth\n size=\"small\"\n // A Select has no native placeholder: the hint is what the closed\n // control displays while nothing is chosen, which needs\n // `displayEmpty` (MUI renders nothing for `''` without it).\n slotProps={\n hint\n ? {\n ...hintSlotProps,\n select: {\n displayEmpty: true,\n renderValue: (value: unknown) =>\n value ? (\n String(value)\n ) : (\n <span style={{ opacity: 0.6 }}>{hint}</span>\n ),\n },\n }\n : undefined\n }\n {...rest}\n >\n {choices.map((choice, index) => (\n <MenuItem key={index} value={choice}>\n {choice}\n </MenuItem>\n ))}\n </TextField>\n {mapInput}\n {consentNote}\n </>\n )\n }\n\n if (fieldType === 'radio') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <RadioGroup name={name}>\n {choices.map((choice, index) => (\n <FormControlLabel\n key={index}\n value={choice}\n label={choice}\n control={<Radio size=\"small\" required={Boolean(required)} />}\n />\n ))}\n </RadioGroup>\n {mapInput}\n {consentNote}\n </FormControl>\n )\n }\n\n if (fieldType === 'checkbox') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <FormGroup>\n {choices.map((choice, index) => (\n <FormControlLabel\n key={index}\n label={choice}\n control={\n <Checkbox\n name={name}\n value={choice}\n size=\"small\"\n required={Boolean(required) && checkedCount === 0}\n onChange={(event) =>\n setCheckedCount(\n (count) => count + (event.target.checked ? 1 : -1),\n )\n }\n />\n }\n />\n ))}\n </FormGroup>\n {mapInput}\n {consentNote}\n </FormControl>\n )\n }\n\n if (fieldType === 'rating') {\n return (\n <FormControl ref={ref} required={Boolean(required)} {...rest}>\n <FormLabel>{fieldLabel}</FormLabel>\n <Rating\n // `__`-prefixed so the star radios themselves are skipped by\n // Form.handleSubmit; the hidden input carries the value.\n name={`__${name}`}\n value={rating}\n onChange={(_event, value) => setRating(value)}\n />\n <input type=\"hidden\" name={name} value={rating ?? ''} />\n {mapInput}\n {consentNote}\n </FormControl>\n )\n }\n\n return (\n <>\n <TextField\n ref={ref}\n name={name}\n label={fieldLabel}\n placeholder={hint}\n type={fieldType === 'email' ? 'email' : 'text'}\n required={Boolean(required)}\n multiline={fieldType === 'textarea'}\n minRows={fieldType === 'textarea' ? 3 : undefined}\n fullWidth\n size=\"small\"\n slotProps={hintSlotProps}\n {...rest}\n />\n {mapInput}\n {consentNote}\n </>\n )\n})\nFormField.displayName = 'AglynFormField'\n\nexport const formSchema: Aglyn.ComponentSchema<FormProps> = {\n $id: FORM_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Form',\n description:\n 'Collects its Form Fields and sends the answers to you, or into a dataset.',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiEmailFastOutline.path, sx: { color: 'info.main' } },\n attributes: [\n {\n name: 'formId',\n description:\n 'The form this collects for, from the Forms page. Stored by id, so ' +\n 'renaming the form never splits its submission history and two ' +\n 'pages placing the same form share one list. Leave unset to keep ' +\n 'filing submissions under the name below.',\n component: Aglyn.FieldComponentType.FORM_SELECT,\n label: 'Form',\n },\n {\n name: 'formName',\n description:\n 'Caption shown in the submissions inbox, used when no form above ' +\n 'is picked. A name is not an identity: renaming it splits the ' +\n 'history of everything already submitted under the old one.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Form name',\n },\n {\n name: 'datasetId',\n description:\n 'Optional dataset (from the Data page) submissions are appended ' +\n 'to. Stored by id, so renaming the dataset never breaks the ' +\n 'binding (AGL-556). The inbox always gets a copy.',\n component: Aglyn.FieldComponentType.DATASET_SELECT,\n label: 'Write to dataset',\n },\n {\n name: 'datasetName',\n description:\n 'Legacy name-based dataset binding — matched against the ' +\n \"dataset's display name at submit time. Pick the dataset above \" +\n 'and clear this; it only shows while a name is still set.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Write to dataset (legacy name)',\n condition: { when: 'datasetName', isNotEmpty: true },\n },\n {\n name: 'submitLabel',\n description: 'Label of the submit button.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Submit label',\n },\n {\n name: 'successMessage',\n description: 'Shown after a successful submission.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Success message',\n },\n // After-submit outcomes (AGL-557).\n {\n name: 'afterSubmit',\n description:\n 'What a successful submit does: show the success message, send ' +\n 'the visitor to another page, or reveal a hidden element.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'After submit',\n // `message` is the first member of the declared `FormAfterSubmit`\n // union, so the sentinel already existed in the type and only the\n // option list was spelling it `''` (AGL-1451) — a value the\n // attributes form strips on change (AGL-1191), which meant a form\n // switched to Redirect could not be switched back to its message.\n options: [\n { value: 'message', label: 'Show the success message' },\n { value: 'redirect', label: 'Redirect the visitor' },\n { value: 'reveal', label: 'Reveal a hidden element' },\n ],\n },\n {\n name: 'redirectScreenId',\n description:\n 'Screen the visitor lands on after submitting. The address is ' +\n \"resolved at render time, so renaming the screen's slug never \" +\n 'breaks the redirect.',\n component: Aglyn.FieldComponentType.SCREEN_SELECT,\n label: 'Redirect to screen',\n condition: { when: 'afterSubmit', is: 'redirect' },\n },\n {\n name: 'redirectUrl',\n description:\n 'Used only when no screen is selected above: a same-site path ' +\n '(/thanks) or an https URL. Anything else is ignored.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Redirect URL',\n condition: { when: 'afterSubmit', is: 'redirect' },\n },\n {\n name: 'revealNodeId',\n description:\n 'Element shown after a successful submit — it stays hidden on ' +\n 'the published page until then.',\n component: Aglyn.FieldComponentType.NODE_SELECT,\n label: 'Element to reveal',\n condition: { when: 'afterSubmit', is: 'reveal' },\n },\n ],\n}\n\nexport const formFieldSchema: Aglyn.ComponentSchema<FormFieldProps> = {\n $id: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Form Field',\n description:\n 'One input inside a form — text, select, radio, checkbox or rating.',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n flags: {\n selfClosing: Aglyn.FEATURE_FLAG.ENABLED,\n },\n attributes: [\n {\n name: 'fieldName',\n description: 'Key this value is stored under in submissions.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Field name',\n },\n {\n name: 'datasetFieldId',\n description:\n \"Schema field of the form's dataset this value is stored under. \" +\n 'Stored by id, so renaming the field never breaks the mapping ' +\n '(AGL-556). When unset, values match dataset fields by name.',\n component: Aglyn.FieldComponentType.DATASET_FIELD_SELECT,\n label: 'Maps to schema field',\n },\n {\n name: 'label',\n description: 'Visible input label.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Label',\n },\n {\n name: 'placeholder',\n description:\n 'Grey hint inside the empty input — \"you@company.com\" (AGL-1330). ' +\n 'It is not a label: it disappears as soon as the visitor types, so ' +\n 'always keep Label set. Ignored by radio, checkbox, and rating ' +\n 'fields. Clearing it removes the hint.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Placeholder',\n },\n {\n name: 'fieldType',\n description: 'Input type.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'Type',\n // `text` is the first member of this prop's own union, and the value\n // the render falls back to. As `''` it was the one field type an\n // author could not choose and keep (AGL-1191/AGL-1451): a field\n // switched to Email or Multiline could never be made plain text\n // again without deleting it.\n options: [\n { value: 'text', label: 'Text' },\n { value: 'email', label: 'Email' },\n { value: 'textarea', label: 'Multiline' },\n { value: 'select', label: 'Dropdown' },\n { value: 'radio', label: 'Radio choice' },\n { value: 'checkbox', label: 'Checkboxes' },\n { value: 'rating', label: 'Star rating' },\n ],\n },\n {\n name: 'options',\n description:\n 'Choices for dropdown, radio, and checkbox fields — one per line ' +\n '(or comma-separated). Ignored by the other types.',\n component: Aglyn.FieldComponentType.TEXTAREA,\n label: 'Options',\n },\n {\n name: 'required',\n description: 'Whether the field must be filled.',\n component: Aglyn.FieldComponentType.SWITCH,\n label: 'Required?',\n },\n ],\n}\n\nexport const formPresets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(FORM_ID, 'contact'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Contact Form',\n pluginId: BUNDLE_ID,\n description: 'Name, email, and message with a send button',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiEmailFastOutline.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_ID,\n pluginId: BUNDLE_ID,\n props: { formName: 'Contact' },\n nodes: [\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName: 'name', label: 'Name', required: true },\n },\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: {\n fieldName: 'email',\n label: 'Email',\n fieldType: 'email',\n required: true,\n },\n },\n {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: {\n fieldName: 'message',\n label: 'Message',\n fieldType: 'textarea',\n required: true,\n },\n },\n ],\n },\n },\n {\n $id: generatePresetId(FORM_FIELD_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Form Field',\n pluginId: BUNDLE_ID,\n description: 'Single input inside a form',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName: 'field', label: 'Field' },\n },\n },\n /*\n * The marketing consent field as one preset rather than a recipe. Its props\n * are the platform's `MARKETING_CONSENT_FORM_FIELD`, which every place that adds\n * this field reads, so each adds the same unticked one-option checkbox. It\n * records consent once the form names it as its Marketing consent field.\n */\n {\n $id: generatePresetId(FORM_FIELD_ID, 'marketing-consent'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Marketing consent',\n pluginId: BUNDLE_ID,\n description: 'An unticked checkbox asking permission to send marketing emails',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiFormTextbox.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { ...Aglyn.MARKETING_CONSENT_FORM_FIELD },\n },\n },\n]\n\n/**\n * The composed section a form usually arrives inside.\n *\n * It lives here rather than with the generic block library because it PLACES\n * form nodes, and a preset's `data` carries the `pluginId` those nodes are\n * saved with — so authoring it anywhere else stamps every contact form on\n * every new site with the wrong bundle. The heading and the stack around it\n * name `mui`, which is correct: they ARE mui elements, and a page holding this\n * section needs both bundles registered before it paints.\n */\nexport const formBlockPresets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(FORM_ID, 'contact-section'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Contact Section',\n pluginId: BUNDLE_ID,\n description: 'Heading with a name/email/message form',\n category: Aglyn.ComponentCategory.BLOCKS,\n icon: { path: mdiEmailOutline.path, sx: { color: 'info.main' } },\n data: {\n $id: null,\n componentId: 'muiStack',\n pluginId: MUI_BUNDLE_ID,\n props: { spacing: 2 },\n sx: {\n paddingLeft: 4,\n paddingRight: 4,\n paddingTop: 6,\n paddingBottom: 6,\n maxWidth: 560,\n },\n nodes: [\n {\n $id: null,\n componentId: 'muiTypography',\n pluginId: MUI_BUNDLE_ID,\n props: { variant: 'h4', children: 'Get in touch' },\n },\n {\n $id: null,\n componentId: FORM_ID,\n pluginId: BUNDLE_ID,\n props: {\n formName: 'Contact',\n submitLabel: 'Send message',\n successMessage: 'Thanks — we will get back to you soon.',\n },\n nodes: [\n contactField('name', 'Name'),\n contactField('email', 'Email', { fieldType: 'email' }),\n contactField('message', 'Message', { fieldType: 'textarea' }),\n ],\n },\n ],\n },\n },\n]\n\nfunction contactField(fieldName: string, label: string, extra?: object) {\n return {\n $id: null,\n componentId: FORM_FIELD_ID,\n pluginId: BUNDLE_ID,\n props: { fieldName, label, required: true, ...extra },\n }\n}\n\nexport { Form, FormField }\nexport default Form\n"],"names":["sendAnalyticsBeacon","trackEventBeforeNavigation","campaignTouchField","Aglyn","mdiEmailFastOutline","mdiEmailOutline","mdiFormTextbox","isSameOriginPath","Alert","Button","Checkbox","FormControl","FormControlLabel","FormGroup","FormHelperText","FormLabel","Link","MenuItem","Radio","RadioGroup","Rating","Stack","TextField","createContext","forwardRef","useCallback","useContext","useEffect","useRef","useState","BUNDLE_ID","generatePresetId","MUI_BUNDLE_ID","FORM_ID","FORM_FIELD_ID","FIELD_MAP_INPUT_PREFIX","CONSENT_GROUP_INPUT","FormConsentDisclosureContext","readFormConsentDisclosure","body","value","text","trim","key","fieldName","sanitizeRedirectUrl","url","trimmed","undefined","test","resolveRedirectTarget","screenHref","redirectUrl","REVEAL_NODE_ID_PATTERN","formNavigation","assign","window","location","FORM_SUBMITTED_EVENT","sendFormBeacon","hostId","formId","event","form","Form","props","ref","formName","datasetId","_datasetId","datasetName","_datasetName","datasetBindingToken","submitLabel","successMessage","afterSubmit","redirectScreenId","revealNodeId","children","rest","useSite","siteFetch","useSiteFetch","href","redirectScreenHref","suppressNavigation","useScreenLink","revealSelector","status","setStatus","reported","view","start","current","handleFirstInput","pausedMessage","setPausedMessage","unavailable","setUnavailable","alerts","setAlerts","disclosure","setDisclosure","query","URLSearchParams","toString","then","response","ok","json","catch","handleSubmit","preventDefault","data","FormData","currentTarget","fields","website","consentGroup","entries","startsWith","slice","method","headers","JSON","stringify","datasetBinding","path","pathname","payload","Array","isArray","dispatchEvent","CustomEvent","detail","form_name","form_location","target","isPreviewRefusal","paused","parseLockdownRefusal","lockdownRefusalText","notice","parseFormUnavailableRefusal","severity","component","spacing","onSubmit","onInput","style","Provider","input","type","name","tabIndex","autoComplete","aria-hidden","position","left","height","width","message","contact","map","alert","index","variant","disabled","sx","alignSelf","displayName","parseFieldOptions","options","split","entry","filter","Boolean","FormField","datasetFieldId","label","placeholder","fieldType","required","fieldLabel","choices","hint","hintSlotProps","inputLabel","shrink","notched","checkedCount","setCheckedCount","rating","setRating","mapInput","readOnly","describesOptIn","isMarketingConsentFieldName","consentNote","select","defaultValue","fullWidth","size","slotProps","displayEmpty","renderValue","String","span","opacity","choice","control","onChange","count","checked","_event","multiline","minRows","formSchema","$id","pluginId","description","category","ComponentCategory","FORMS","icon","color","attributes","FieldComponentType","FORM_SELECT","TEXT_FIELD","DATASET_SELECT","condition","when","isNotEmpty","SELECT","SCREEN_SELECT","is","NODE_SELECT","formFieldSchema","flags","selfClosing","FEATURE_FLAG","ENABLED","DATASET_FIELD_SELECT","TEXTAREA","SWITCH","formPresets","NodeType","PRESET","componentId","nodes","MARKETING_CONSENT_FORM_FIELD","formBlockPresets","BLOCKS","paddingLeft","paddingRight","paddingTop","paddingBottom","maxWidth","contactField","extra"],"mappings":"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"}
@@ -531,7 +531,7 @@ const FORM_HIDDEN_COLUMNS = {
531
531
  /*#__PURE__*/ _jsxs(CardDisplay, {
532
532
  header: "Forms",
533
533
  help: pluginDocsHelp('forms', {
534
- anchor: '#build-a-form',
534
+ anchor: '#find-a-form',
535
535
  excerpt: 'A form collects submissions into the Inbox, and its design is ' + 'drawn in the besigner like any other artifact.'
536
536
  }),
537
537
  children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/host-forms-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type { AglynOrgBilling } from '@aglyn/aglyn'\nimport {\n buildRoute,\n isFormArchived,\n PageHeaderActions,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { ICON_VARIANT_SHOW_DETAIL } from '@aglyn/shared-data-enums'\nimport {\n mdiArchiveArrowDownOutline,\n mdiArchiveArrowUpOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiVectorSquare,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListTable, {\n ListRowActions,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n inMemoryListField,\n type ListFilterClause,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { usePagedRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-paged-rows-filter'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, updateDoc } from 'firebase/firestore'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useHostResourceApi,\n useLiveArtifactCount,\n usePagedCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { collectionPage } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/*\n * What the forms grid's Filters panel offers (AGL-3317). The list is a paged\n * listener, so a filter matches over what its window has read, which the\n * first filter widens (`usePagedRowsFilter`). Status is a hidden column\n * reading `statusKey`, whether the form is retired.\n */\nconst FORM_FILTER_FIELDS = [\n inMemoryListField('displayName', 'text'),\n inMemoryListField('slug', 'text'),\n inMemoryListField('status', 'select', 'statusKey'),\n]\nconst FORM_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n slug: 'Slug',\n status: 'Status',\n}\nconst FORM_FILTER_OPTIONS = {\n status: [\n { value: 'active', label: 'Active' },\n { value: 'retired', label: 'Retired' },\n ],\n}\nconst FORM_SEARCH_FIELDS = ['displayName', 'slug'] as const\nconst FORM_HIDDEN_COLUMNS = { status: false }\n\n/** A form row with the status its filter matches on. */\nconst withStatusKey = (form: any) => ({\n ...form,\n statusKey: isFormArchived(form) ? 'retired' : 'active',\n})\n\nexport interface HostFormsCardProps {\n hostId: string\n /**\n * The Forms surface's own absolute console path, from the shell.\n *\n * A row's link is this plus the form's id, resolved synchronously. The\n * alternative is `useConsoleHostRoute`, which answers `null` for a paint\n * while it reads two documents — and a table whose every row links to\n * `/null/...` on first render is worse than one that pays nothing.\n */\n basePath?: string\n /**\n * The org billing document, from the shell.\n *\n * The quota readout's denominator resolves from it. `undefined` means the\n * plan is not known rather than that it is Free — the shell holds this\n * surface behind a spinner until the org read settles, so it reads that way\n * only when the read failed — and the readout says so instead of naming a\n * cap it cannot support.\n */\n org?: Partial<AglynOrgBilling>\n}\n\n/**\n * THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.\n *\n * The list is the components list's table, deliberately and not\n * approximately: `ListTable` gives it the row grammar every artifact list has\n * — the row opens the detail page, rows are not selectable, one quick action\n * then the overflow — and `ListPagination` gives it the console's one footer.\n * A form is an artifact like a component, so a reader who has used one list\n * has used this one.\n *\n * ## Why the empty state is a table and not a sentence\n *\n * A paragraph and a button reads as a smaller feature than this is: a form is\n * a thing with a slug, a submission count and a version history, and none of\n * that would be visible until the reader had already committed to making one.\n * Rendering the columns with the empty overlay inside them teaches the shape\n * of the artifact BEFORE the first one exists, which is what the components\n * list has always done.\n *\n * ## The two numeric columns, and when one is honestly blank\n *\n * `stats.submissions` and `stats.leads` are both incremented by\n * `/api/forms/submit` on a write that was happening anyway, so both are real\n * numbers. `leads` is counted only for a form whose `routing.lead` is set, so\n * a form that does not route to leads carries no figure at all and renders as\n * a dash rather than as `0`. A zero would say this form has produced no leads,\n * which for an unrouted form is a measurement nobody took.\n */\nexport function HostFormsCard(props: HostFormsCardProps) {\n const { hostId, basePath, org } = props\n const router = useRouter()\n // Duplicate (AGL-2936): the copy is a form of its own — its submissions\n // start empty — and opens on its detail page once made.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) => router.push(formHref(copy.id)),\n })\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n /**\n * The shell's zone renderer (AGL-3043), for other ways to start a form; `null`\n * outside the console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n\n /**\n * One form's page, beneath this surface's own path.\n *\n * `basePath` is the shell's answer and needs no read; the route table is the\n * fallback for a caller that has none, and it is the one that can be `null`\n * for a paint.\n */\n const formHref = useCallback(\n (formId: string) =>\n basePath\n ? `${basePath}/${formId}`\n : buildRoute(Route.FORM_DETAILS, {\n orgSlug: orgSlug ?? '',\n host: host ?? '',\n formId,\n }),\n [basePath, orgSlug, host],\n )\n\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const [creating, setCreating] = useState(false)\n const [retireError, setRetireError] = useState<string | null>(null)\n\n /**\n * Retire a form, or put it back (AGL-2671).\n *\n * A write of one field, client-side like every other edit on this document:\n * the rules already admit it for a host writer, fencing off only `stats`.\n *\n * ⛔ NOT a delete, and the difference is the whole feature. The submissions,\n * the leads and the contact timeline they built are filed under this form,\n * so the document is what makes that history readable; deleting it to stop\n * collection would throw away the reason the merchant kept the form.\n *\n * `Date.now()` is the browser's clock rather than `serverTimestamp()`\n * because {@link isFormArchived} asks for a number greater than zero, and a\n * server sentinel reads as `null` on the local echo — the row would stay\n * looking live until the write round-tripped. The value is a marker, never\n * an audit fact: nothing computes a duration from it.\n */\n const setRetired = useCallback(\n async (formId: string, retired: boolean) => {\n setRetireError(null)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n archivedAt: retired ? Date.now() : null,\n })\n } catch {\n setRetireError(\n retired\n ? 'Could not retire that form. Try again shortly.'\n : 'Could not restore that form. Try again shortly.',\n )\n }\n },\n [firestore, hostId],\n )\n\n /**\n * The list PAGES, over an ordered walk.\n *\n * `collectionPage` holds the ordering decision and the reason it is the\n * document id rather than `displayName` — briefly, `orderBy` matches only\n * documents that HAVE the field, and the resources route stores an\n * allow-list it never checks for presence, so ordering on a name would hide\n * every form created without one rather than mis-sorting the list.\n *\n * The page is NOT re-sorted: sorting a server-ordered window in the browser\n * is what makes a pseudo-random sample look like the first page.\n */\n const {\n status,\n data: formData,\n rows: formWindow,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n collectionPage(\n collection(firestore, 'hosts', hostId, 'forms'),\n pageLimit,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n /*\n * A retired form is a TOMBSTONE, not a row — unless the reader asks for the\n * tombstones (AGL-2671). Client-side because it has to be: Firestore cannot\n * ask for the ABSENCE of a field, so a form created through the resources\n * route (which carries no `archivedAt` at all) and one archived later are\n * not one value to filter on. The cost is that a tombstone spends a slot in\n * whichever page it falls in.\n *\n * The Filters panel's Status is how the reader asks (AGL-3317). With no\n * Status clause the list leaves the tombstones out, as it always has, and\n * reads one page; a Status clause hands the panel every row the window\n * read, retired included, and the clause itself picks which to show. That\n * default is the absence of a clause rather than a clause the list starts\n * with, because a starting clause would widen the window on every visit to\n * answer a question nobody asked. Retiring a form removes it from the list,\n * and without the Status filter that would be a one-way door: the row is\n * the only route back to Restore.\n */\n const [formClauses, setFormClauses] = useState<ListFilterClause[]>([])\n const askedStatus = formClauses.some((clause) => clause.field === 'status')\n const formRows = useMemo(\n () =>\n formWindow\n .filter((form: any) => askedStatus || !isFormArchived(form))\n .map(withStatusKey),\n [formWindow, askedStatus],\n )\n const formWindowRows = useMemo(\n () =>\n formData\n ?.filter((form: any) => askedStatus || !isFormArchived(form))\n .map(withStatusKey),\n [formData, askedStatus],\n )\n const formFilter = usePagedRowsFilter<any>(\n {\n data: formWindowRows,\n rows: formRows,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n },\n {\n fields: FORM_FILTER_FIELDS,\n options: FORM_FILTER_OPTIONS,\n headers: FORM_FILTER_HEADERS,\n search: FORM_SEARCH_FIELDS,\n clauses: formClauses,\n onChange: setFormClauses,\n },\n )\n // The rows on screen: the page, or the page of the matches.\n const forms = formFilter.rows\n\n /*\n * The COUNT is a server aggregate, not the length of a page. `forms` is one\n * page, and publishing that as the site's form count would read as room to\n * spare on a site that is already at the ceiling.\n */\n const liveFormCount = useLiveArtifactCount(hostId, 'forms')\n // Pending or refused, the page window stands in: a LOWER bound, never a\n // confident zero.\n const formsUsed = liveFormCount ?? formWindow.length\n\n /**\n * Name first, then create (AGL-700).\n *\n * A form is created with BOTH halves seeded. `fields` is the declaration the\n * submission path reads and starts empty; the canvas is the design, seeded\n * with a root and a form node already bound to this id — so the besigner\n * opens on something that satisfies `checkFormContract` rather than on a\n * blank page whose first publish is a list of violations.\n */\n const handleCreate = useCallback(\n async (values: Record<string, any>) => {\n if (creating) return\n setCreating(true)\n setCreateError(null)\n try {\n const formId = Aglyn.createResourceUid()\n await createHostResource({\n hostId,\n resource: 'form',\n id: formId,\n data: {\n displayName: values['displayName'],\n slug: Aglyn.normalizeFormSlug(values['displayName']) || formId,\n fields: [],\n rootId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n nodes: {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: ['formRoot'],\n },\n formRoot: {\n $id: 'formRoot',\n componentId: 'form',\n pluginId: BUNDLE_ID,\n parentId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n props: { formId, formName: values['displayName'] },\n nodes: [],\n },\n },\n },\n })\n setCreateOpen(false)\n router.push(formHref(formId))\n } catch (error) {\n console.error(error)\n setCreateError('Could not create that form')\n } finally {\n setCreating(false)\n }\n },\n [creating, createHostResource, hostId, router, formHref],\n )\n\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Display name',\n minWidth: 220,\n type: 'string',\n renderCell: ({ id, value }: any) => (\n <AppLink href={formHref(id as string)}>\n {value || (id as string)}\n </AppLink>\n ),\n },\n {\n field: 'slug',\n headerName: 'Slug',\n minWidth: 160,\n flex: 1,\n type: 'string',\n // Blank reads as a rendering gap; '--' reads as \"nothing here\".\n valueFormatter: (value: any) => value || '--',\n },\n {\n field: 'submissions',\n headerName: 'Submissions',\n minWidth: 130,\n type: 'number',\n // Head AND body. `type: 'number'` right-aligns both, and the explicit\n // pair says so at the call site rather than relying on a grid default\n // that a later `renderCell` would silently override.\n align: 'right',\n headerAlign: 'right',\n valueGetter: (_value: any, row: any) => row?.stats?.submissions ?? null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'leads',\n headerName: 'Leads',\n minWidth: 100,\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n /*\n * A dash rather than a `0` whenever the field is absent.\n *\n * `stats.leads` is incremented only for a form whose `routing.lead` is\n * set, so a form that does not route to leads has no recorded figure.\n * Rendering `0` there would read as \"this form has produced no leads\",\n * which is a claim about a measurement nobody took.\n */\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.leads === 'number' ? row.stats.leads : null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'lastSubmission',\n headerName: 'Last submission',\n minWidth: 170,\n flex: 1,\n type: 'date',\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.lastSubmissionAtMs === 'number'\n ? new Date(row.stats.lastSubmissionAtMs)\n : null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n minWidth: 170,\n flex: 1,\n type: 'date',\n // MUI X v9 passes the value positionally. The v6 object form silently\n // destructures undefined off a Date and every row renders '--'.\n valueGetter: (value: any) => value?.toDate?.() ?? null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n listActionsColumn((row: any) => {\n const form = { ...row, $id: row.$id as string }\n const versionId = form.versionId as string | undefined\n return (\n <ListRowActions\n label={form.displayName ?? form.$id}\n quick={{\n icon: mdiEyeOutline.path,\n label: 'Preview',\n // A form with no version has never been opened in the besigner, so\n // there is no snapshot to render. Disabled and saying so, rather\n // than a link to an empty preview.\n ...(versionId && orgSlug && host\n ? {\n to: buildRoute(Route.FORM_PREVIEW, {\n orgSlug,\n host,\n formId: form.$id,\n versionId,\n }),\n }\n : {\n unavailableReason: versionId\n ? 'Resolving this site’s address…'\n : 'Nothing to preview yet — open it in the besigner once.',\n }),\n }}\n items={[\n {\n key: 'details',\n label: 'View details',\n icon: <MdiIcon path={ICON_VARIANT_SHOW_DETAIL.path} size={0.8} />,\n href: formHref(form.$id),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiVectorSquare.path} size={0.8} />,\n /*\n * A LINK only once the form has a version. A form that has never\n * been opened has none, and opening it MINTS the first one —\n * that is a write, and the detail page is the one place that\n * decides what an initial version looks like. Sending the reader\n * there rather than minting a second way is what keeps the two\n * from drifting.\n */\n href:\n versionId && orgSlug && host\n ? buildRoute(Route.FORM_BESIGNER, {\n orgSlug,\n host,\n formId: form.$id,\n versionId,\n })\n : formHref(form.$id),\n },\n /*\n * Retire / Restore (AGL-2671). Last in the menu, and the only item\n * here that writes.\n *\n * The label says what it DOES to the form rather than what it does\n * to this list — \"Archive\" reads like a filing action, and the\n * consequence a merchant needs to weigh is that the form stops\n * collecting. `/api/forms/submit` refuses a retired form, so one\n * still placed on a published page will turn visitors away.\n */\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('form', {\n id: form.$id,\n name: form.displayName ?? '',\n }),\n },\n isFormArchived(form)\n ? {\n key: 'restore',\n label: 'Restore — start collecting again',\n icon: <MdiIcon path={mdiArchiveArrowUpOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, false),\n }\n : {\n key: 'retire',\n label: 'Retire — stop collecting, keep the history',\n icon: <MdiIcon path={mdiArchiveArrowDownOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, true),\n },\n ]}\n />\n )\n }),\n ]\n\n return (\n <>\n {duplicate.dialog}\n {/*\n The readout leads the create button, in the PAGE header — where Sites,\n screens, layouts, components and templates put theirs. Forms declares\n no sections, so it has no vertical rail for a card-header cluster to\n belong beside, and the controls are about the whole surface rather\n than about anything the card is showing.\n Published from the card because the card owns what they say: the count\n comes from the listener already open here, and a page that counted for\n itself would be a second source for one fact and a second read for one\n collection. It publishes from the LIST, so a form's own detail route —\n a different component — leaves the header with nothing to create into.\n */}\n <PageHeaderActions>\n <Stack direction=\"row\" spacing={2} sx={{ alignItems: 'center' }}>\n {/*\n The denominator is `formsPerHost` — the allowance\n `/api/hosts/resources` refuses the create at, resolved through the\n same `checkQuota` the route calls, so the number offered and the\n number enforced cannot drift apart. `FORMS_MAX_PER_HOST` bounds a\n READ of the catalog and sits above every plan's allowance, so\n quoting it advertises room the server will not honor.\n */}\n <QuotaReadoutComponent\n ready={org != null}\n used={formsUsed}\n limit={Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit}\n noun=\"form\"\n />\n {/*\n A retired form keeps its slot, and the readout says so rather than\n leaving a merchant to work out why a catalog of three is using five\n (AGL-2674). The count comes from the same aggregate\n `/api/hosts/resources` enforces on, which subtracts deleted forms\n and nothing else — so retirement is deliberately not a way to buy\n room back, and the number here is the number the server will\n refuse a create at.\n\n Shown only when a retirement is actually in the loaded page: the\n line answers a question nobody is asking on a site that has never\n retired anything. It makes no numeric claim, because one page is a\n lower bound on how many there are.\n */}\n {formWindow.some((form: any) => isFormArchived(form)) ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Retired forms keep their slot'}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n {/*\n Other ways to start a form, from plugins (AGL-3043): the\n `hostForms` zone, drawn through the shell's own gated slot. A\n plugin page cannot mount that slot itself, so the shell hands it\n down, and a widget here passes the gates a console page's would.\n */}\n {CreateZone ? (\n <CreateZone slot=\"hostForms\" hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={creating}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {creating ? 'Creating…' : 'Create Form'}\n </Button>\n </Stack>\n </Stack>\n </PageHeaderActions>\n <CardDisplay\n header=\"Forms\"\n help={pluginDocsHelp('forms', {\n anchor: '#build-a-form',\n excerpt:\n 'A form collects submissions into the Inbox, and its design is ' +\n 'drawn in the besigner like any other artifact.',\n })}\n >\n {retireError ? (\n <Alert severity=\"error\" sx={{ mb: 2 }}>\n {retireError}\n </Alert>\n ) : null}\n <ListFilterChips {...formFilter.chipsProps} />\n {formFilter.filtering && hasMore ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Filtering the ${formFilter.read} forms read so far — the next page reads more.`}\n </Typography>\n ) : null}\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={formFilter.filterColumns(columns)}\n initialState={{ columns: { columnVisibilityModel: FORM_HIDDEN_COLUMNS } }}\n // The panel and the search are the grid's; the card answers them\n // over what its window read.\n {...formFilter.gridProps}\n {...(formFilter.filtering\n ? { noRowsLabel: 'No forms match these filters' }\n : {\n noRowsLabel: 'No forms yet',\n noRowsDescription:\n 'A form collects submissions, dedupes the people who send them, and can route them to a lead. Its design is drawn in the besigner and published like any other artifact.',\n noRowsAction: (\n <Button variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'Create your first form'}\n </Button>\n ),\n })}\n rows={forms}\n // The whole row opens the detail page; the action cluster stops\n // propagation so a menu click never navigates underneath it.\n onOpen={(id) => router.push(formHref(String(id)))}\n // An empty table while the read is in flight reads as \"you have none\"\n // rather than \"these are on their way\".\n loading={status === 'loading'}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n <ListPagination {...formFilter.pagination} />\n {/*\n The console's own create drawer, from the shared library rather than a\n second one that looks like it. The empty state and the header open the\n SAME one, so the state that says \"creating…\" is one state.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new form\"\n // A form document stores no description: `/api/hosts/resources` filters\n // `data` through a per-kind allow-list, so one typed here is dropped\n // without a word.\n includeDescription={false}\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {createError}\n </Alert>\n ) : null\n }\n />\n </CardDisplay>\n </>\n )\n}\nHostFormsCard.displayName = 'HostFormsCard'\n\nexport default HostFormsCard\n"],"names":["Aglyn","buildRoute","isFormArchived","PageHeaderActions","pluginDocsHelp","Route","useConsoleWidgetSlot","ICON_VARIANT_SHOW_DETAIL","mdiArchiveArrowDownOutline","mdiArchiveArrowUpOutline","mdiContentCopy","mdiEyeOutline","mdiVectorSquare","AppLink","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListTable","ListRowActions","listActionsColumn","inMemoryListField","TABLE_ROW_HEIGHT","usePagedRowsFilter","QuotaReadoutComponent","CreateArtifactDrawer","Alert","Button","Stack","Typography","collection","doc","updateDoc","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useHostResourceApi","useLiveArtifactCount","usePagedCollection","collectionPage","useRouter","useCallback","useMemo","useState","BUNDLE_ID","FORM_FILTER_FIELDS","FORM_FILTER_HEADERS","displayName","slug","status","FORM_FILTER_OPTIONS","value","label","FORM_SEARCH_FIELDS","FORM_HIDDEN_COLUMNS","withStatusKey","form","statusKey","HostFormsCard","props","hostId","basePath","org","router","duplicate","onDuplicated","_kind","copy","push","formHref","id","orgSlug","subdomain","host","firestore","createHostResource","CreateZone","formId","FORM_DETAILS","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","retireError","setRetireError","setRetired","retired","archivedAt","Date","now","data","formData","rows","formWindow","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","formClauses","setFormClauses","askedStatus","some","clause","field","formRows","filter","map","formWindowRows","formFilter","fields","options","headers","search","clauses","onChange","forms","liveFormCount","formsUsed","length","handleCreate","values","createResourceUid","resource","normalizeFormSlug","rootId","CANVAS_ROOT_ELEMENT_ID","nodes","$id","componentId","formRoot","pluginId","parentId","formName","error","console","columns","headerName","minWidth","type","renderCell","href","flex","valueFormatter","align","headerAlign","valueGetter","_value","row","stats","submissions","toLocaleString","leads","lastSubmissionAtMs","toDate","versionId","quick","icon","path","to","FORM_PREVIEW","unavailableReason","items","key","size","FORM_BESIGNER","onClick","request","name","dialog","direction","spacing","sx","alignItems","ready","used","limit","checkQuota","noun","variant","color","slot","orgId","disabled","header","help","anchor","excerpt","severity","mb","chipsProps","filtering","read","rowHeight","filterColumns","initialState","columnVisibilityModel","gridProps","noRowsLabel","noRowsDescription","noRowsAction","onOpen","String","loading","hideFooter","pagination","open","onClose","title","includeDescription","onSubmit","errorSlot","mt"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/host-forms-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type { AglynOrgBilling } from '@aglyn/aglyn'\nimport {\n buildRoute,\n isFormArchived,\n PageHeaderActions,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { ICON_VARIANT_SHOW_DETAIL } from '@aglyn/shared-data-enums'\nimport {\n mdiArchiveArrowDownOutline,\n mdiArchiveArrowUpOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiVectorSquare,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListTable, {\n ListRowActions,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n inMemoryListField,\n type ListFilterClause,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { usePagedRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-paged-rows-filter'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, updateDoc } from 'firebase/firestore'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useHostResourceApi,\n useLiveArtifactCount,\n usePagedCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { collectionPage } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/*\n * What the forms grid's Filters panel offers (AGL-3317). The list is a paged\n * listener, so a filter matches over what its window has read, which the\n * first filter widens (`usePagedRowsFilter`). Status is a hidden column\n * reading `statusKey`, whether the form is retired.\n */\nconst FORM_FILTER_FIELDS = [\n inMemoryListField('displayName', 'text'),\n inMemoryListField('slug', 'text'),\n inMemoryListField('status', 'select', 'statusKey'),\n]\nconst FORM_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n slug: 'Slug',\n status: 'Status',\n}\nconst FORM_FILTER_OPTIONS = {\n status: [\n { value: 'active', label: 'Active' },\n { value: 'retired', label: 'Retired' },\n ],\n}\nconst FORM_SEARCH_FIELDS = ['displayName', 'slug'] as const\nconst FORM_HIDDEN_COLUMNS = { status: false }\n\n/** A form row with the status its filter matches on. */\nconst withStatusKey = (form: any) => ({\n ...form,\n statusKey: isFormArchived(form) ? 'retired' : 'active',\n})\n\nexport interface HostFormsCardProps {\n hostId: string\n /**\n * The Forms surface's own absolute console path, from the shell.\n *\n * A row's link is this plus the form's id, resolved synchronously. The\n * alternative is `useConsoleHostRoute`, which answers `null` for a paint\n * while it reads two documents — and a table whose every row links to\n * `/null/...` on first render is worse than one that pays nothing.\n */\n basePath?: string\n /**\n * The org billing document, from the shell.\n *\n * The quota readout's denominator resolves from it. `undefined` means the\n * plan is not known rather than that it is Free — the shell holds this\n * surface behind a spinner until the org read settles, so it reads that way\n * only when the read failed — and the readout says so instead of naming a\n * cap it cannot support.\n */\n org?: Partial<AglynOrgBilling>\n}\n\n/**\n * THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.\n *\n * The list is the components list's table, deliberately and not\n * approximately: `ListTable` gives it the row grammar every artifact list has\n * — the row opens the detail page, rows are not selectable, one quick action\n * then the overflow — and `ListPagination` gives it the console's one footer.\n * A form is an artifact like a component, so a reader who has used one list\n * has used this one.\n *\n * ## Why the empty state is a table and not a sentence\n *\n * A paragraph and a button reads as a smaller feature than this is: a form is\n * a thing with a slug, a submission count and a version history, and none of\n * that would be visible until the reader had already committed to making one.\n * Rendering the columns with the empty overlay inside them teaches the shape\n * of the artifact BEFORE the first one exists, which is what the components\n * list has always done.\n *\n * ## The two numeric columns, and when one is honestly blank\n *\n * `stats.submissions` and `stats.leads` are both incremented by\n * `/api/forms/submit` on a write that was happening anyway, so both are real\n * numbers. `leads` is counted only for a form whose `routing.lead` is set, so\n * a form that does not route to leads carries no figure at all and renders as\n * a dash rather than as `0`. A zero would say this form has produced no leads,\n * which for an unrouted form is a measurement nobody took.\n */\nexport function HostFormsCard(props: HostFormsCardProps) {\n const { hostId, basePath, org } = props\n const router = useRouter()\n // Duplicate (AGL-2936): the copy is a form of its own — its submissions\n // start empty — and opens on its detail page once made.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) => router.push(formHref(copy.id)),\n })\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n /**\n * The shell's zone renderer (AGL-3043), for other ways to start a form; `null`\n * outside the console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n\n /**\n * One form's page, beneath this surface's own path.\n *\n * `basePath` is the shell's answer and needs no read; the route table is the\n * fallback for a caller that has none, and it is the one that can be `null`\n * for a paint.\n */\n const formHref = useCallback(\n (formId: string) =>\n basePath\n ? `${basePath}/${formId}`\n : buildRoute(Route.FORM_DETAILS, {\n orgSlug: orgSlug ?? '',\n host: host ?? '',\n formId,\n }),\n [basePath, orgSlug, host],\n )\n\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const [creating, setCreating] = useState(false)\n const [retireError, setRetireError] = useState<string | null>(null)\n\n /**\n * Retire a form, or put it back (AGL-2671).\n *\n * A write of one field, client-side like every other edit on this document:\n * the rules already admit it for a host writer, fencing off only `stats`.\n *\n * ⛔ NOT a delete, and the difference is the whole feature. The submissions,\n * the leads and the contact timeline they built are filed under this form,\n * so the document is what makes that history readable; deleting it to stop\n * collection would throw away the reason the merchant kept the form.\n *\n * `Date.now()` is the browser's clock rather than `serverTimestamp()`\n * because {@link isFormArchived} asks for a number greater than zero, and a\n * server sentinel reads as `null` on the local echo — the row would stay\n * looking live until the write round-tripped. The value is a marker, never\n * an audit fact: nothing computes a duration from it.\n */\n const setRetired = useCallback(\n async (formId: string, retired: boolean) => {\n setRetireError(null)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n archivedAt: retired ? Date.now() : null,\n })\n } catch {\n setRetireError(\n retired\n ? 'Could not retire that form. Try again shortly.'\n : 'Could not restore that form. Try again shortly.',\n )\n }\n },\n [firestore, hostId],\n )\n\n /**\n * The list PAGES, over an ordered walk.\n *\n * `collectionPage` holds the ordering decision and the reason it is the\n * document id rather than `displayName` — briefly, `orderBy` matches only\n * documents that HAVE the field, and the resources route stores an\n * allow-list it never checks for presence, so ordering on a name would hide\n * every form created without one rather than mis-sorting the list.\n *\n * The page is NOT re-sorted: sorting a server-ordered window in the browser\n * is what makes a pseudo-random sample look like the first page.\n */\n const {\n status,\n data: formData,\n rows: formWindow,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n collectionPage(\n collection(firestore, 'hosts', hostId, 'forms'),\n pageLimit,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n /*\n * A retired form is a TOMBSTONE, not a row — unless the reader asks for the\n * tombstones (AGL-2671). Client-side because it has to be: Firestore cannot\n * ask for the ABSENCE of a field, so a form created through the resources\n * route (which carries no `archivedAt` at all) and one archived later are\n * not one value to filter on. The cost is that a tombstone spends a slot in\n * whichever page it falls in.\n *\n * The Filters panel's Status is how the reader asks (AGL-3317). With no\n * Status clause the list leaves the tombstones out, as it always has, and\n * reads one page; a Status clause hands the panel every row the window\n * read, retired included, and the clause itself picks which to show. That\n * default is the absence of a clause rather than a clause the list starts\n * with, because a starting clause would widen the window on every visit to\n * answer a question nobody asked. Retiring a form removes it from the list,\n * and without the Status filter that would be a one-way door: the row is\n * the only route back to Restore.\n */\n const [formClauses, setFormClauses] = useState<ListFilterClause[]>([])\n const askedStatus = formClauses.some((clause) => clause.field === 'status')\n const formRows = useMemo(\n () =>\n formWindow\n .filter((form: any) => askedStatus || !isFormArchived(form))\n .map(withStatusKey),\n [formWindow, askedStatus],\n )\n const formWindowRows = useMemo(\n () =>\n formData\n ?.filter((form: any) => askedStatus || !isFormArchived(form))\n .map(withStatusKey),\n [formData, askedStatus],\n )\n const formFilter = usePagedRowsFilter<any>(\n {\n data: formWindowRows,\n rows: formRows,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n },\n {\n fields: FORM_FILTER_FIELDS,\n options: FORM_FILTER_OPTIONS,\n headers: FORM_FILTER_HEADERS,\n search: FORM_SEARCH_FIELDS,\n clauses: formClauses,\n onChange: setFormClauses,\n },\n )\n // The rows on screen: the page, or the page of the matches.\n const forms = formFilter.rows\n\n /*\n * The COUNT is a server aggregate, not the length of a page. `forms` is one\n * page, and publishing that as the site's form count would read as room to\n * spare on a site that is already at the ceiling.\n */\n const liveFormCount = useLiveArtifactCount(hostId, 'forms')\n // Pending or refused, the page window stands in: a LOWER bound, never a\n // confident zero.\n const formsUsed = liveFormCount ?? formWindow.length\n\n /**\n * Name first, then create (AGL-700).\n *\n * A form is created with BOTH halves seeded. `fields` is the declaration the\n * submission path reads and starts empty; the canvas is the design, seeded\n * with a root and a form node already bound to this id — so the besigner\n * opens on something that satisfies `checkFormContract` rather than on a\n * blank page whose first publish is a list of violations.\n */\n const handleCreate = useCallback(\n async (values: Record<string, any>) => {\n if (creating) return\n setCreating(true)\n setCreateError(null)\n try {\n const formId = Aglyn.createResourceUid()\n await createHostResource({\n hostId,\n resource: 'form',\n id: formId,\n data: {\n displayName: values['displayName'],\n slug: Aglyn.normalizeFormSlug(values['displayName']) || formId,\n fields: [],\n rootId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n nodes: {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: ['formRoot'],\n },\n formRoot: {\n $id: 'formRoot',\n componentId: 'form',\n pluginId: BUNDLE_ID,\n parentId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n props: { formId, formName: values['displayName'] },\n nodes: [],\n },\n },\n },\n })\n setCreateOpen(false)\n router.push(formHref(formId))\n } catch (error) {\n console.error(error)\n setCreateError('Could not create that form')\n } finally {\n setCreating(false)\n }\n },\n [creating, createHostResource, hostId, router, formHref],\n )\n\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Display name',\n minWidth: 220,\n type: 'string',\n renderCell: ({ id, value }: any) => (\n <AppLink href={formHref(id as string)}>\n {value || (id as string)}\n </AppLink>\n ),\n },\n {\n field: 'slug',\n headerName: 'Slug',\n minWidth: 160,\n flex: 1,\n type: 'string',\n // Blank reads as a rendering gap; '--' reads as \"nothing here\".\n valueFormatter: (value: any) => value || '--',\n },\n {\n field: 'submissions',\n headerName: 'Submissions',\n minWidth: 130,\n type: 'number',\n // Head AND body. `type: 'number'` right-aligns both, and the explicit\n // pair says so at the call site rather than relying on a grid default\n // that a later `renderCell` would silently override.\n align: 'right',\n headerAlign: 'right',\n valueGetter: (_value: any, row: any) => row?.stats?.submissions ?? null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'leads',\n headerName: 'Leads',\n minWidth: 100,\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n /*\n * A dash rather than a `0` whenever the field is absent.\n *\n * `stats.leads` is incremented only for a form whose `routing.lead` is\n * set, so a form that does not route to leads has no recorded figure.\n * Rendering `0` there would read as \"this form has produced no leads\",\n * which is a claim about a measurement nobody took.\n */\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.leads === 'number' ? row.stats.leads : null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'lastSubmission',\n headerName: 'Last submission',\n minWidth: 170,\n flex: 1,\n type: 'date',\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.lastSubmissionAtMs === 'number'\n ? new Date(row.stats.lastSubmissionAtMs)\n : null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n minWidth: 170,\n flex: 1,\n type: 'date',\n // MUI X v9 passes the value positionally. The v6 object form silently\n // destructures undefined off a Date and every row renders '--'.\n valueGetter: (value: any) => value?.toDate?.() ?? null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n listActionsColumn((row: any) => {\n const form = { ...row, $id: row.$id as string }\n const versionId = form.versionId as string | undefined\n return (\n <ListRowActions\n label={form.displayName ?? form.$id}\n quick={{\n icon: mdiEyeOutline.path,\n label: 'Preview',\n // A form with no version has never been opened in the besigner, so\n // there is no snapshot to render. Disabled and saying so, rather\n // than a link to an empty preview.\n ...(versionId && orgSlug && host\n ? {\n to: buildRoute(Route.FORM_PREVIEW, {\n orgSlug,\n host,\n formId: form.$id,\n versionId,\n }),\n }\n : {\n unavailableReason: versionId\n ? 'Resolving this site’s address…'\n : 'Nothing to preview yet — open it in the besigner once.',\n }),\n }}\n items={[\n {\n key: 'details',\n label: 'View details',\n icon: <MdiIcon path={ICON_VARIANT_SHOW_DETAIL.path} size={0.8} />,\n href: formHref(form.$id),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiVectorSquare.path} size={0.8} />,\n /*\n * A LINK only once the form has a version. A form that has never\n * been opened has none, and opening it MINTS the first one —\n * that is a write, and the detail page is the one place that\n * decides what an initial version looks like. Sending the reader\n * there rather than minting a second way is what keeps the two\n * from drifting.\n */\n href:\n versionId && orgSlug && host\n ? buildRoute(Route.FORM_BESIGNER, {\n orgSlug,\n host,\n formId: form.$id,\n versionId,\n })\n : formHref(form.$id),\n },\n /*\n * Retire / Restore (AGL-2671). Last in the menu, and the only item\n * here that writes.\n *\n * The label says what it DOES to the form rather than what it does\n * to this list — \"Archive\" reads like a filing action, and the\n * consequence a merchant needs to weigh is that the form stops\n * collecting. `/api/forms/submit` refuses a retired form, so one\n * still placed on a published page will turn visitors away.\n */\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('form', {\n id: form.$id,\n name: form.displayName ?? '',\n }),\n },\n isFormArchived(form)\n ? {\n key: 'restore',\n label: 'Restore — start collecting again',\n icon: <MdiIcon path={mdiArchiveArrowUpOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, false),\n }\n : {\n key: 'retire',\n label: 'Retire — stop collecting, keep the history',\n icon: <MdiIcon path={mdiArchiveArrowDownOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, true),\n },\n ]}\n />\n )\n }),\n ]\n\n return (\n <>\n {duplicate.dialog}\n {/*\n The readout leads the create button, in the PAGE header — where Sites,\n screens, layouts, components and templates put theirs. Forms declares\n no sections, so it has no vertical rail for a card-header cluster to\n belong beside, and the controls are about the whole surface rather\n than about anything the card is showing.\n Published from the card because the card owns what they say: the count\n comes from the listener already open here, and a page that counted for\n itself would be a second source for one fact and a second read for one\n collection. It publishes from the LIST, so a form's own detail route —\n a different component — leaves the header with nothing to create into.\n */}\n <PageHeaderActions>\n <Stack direction=\"row\" spacing={2} sx={{ alignItems: 'center' }}>\n {/*\n The denominator is `formsPerHost` — the allowance\n `/api/hosts/resources` refuses the create at, resolved through the\n same `checkQuota` the route calls, so the number offered and the\n number enforced cannot drift apart. `FORMS_MAX_PER_HOST` bounds a\n READ of the catalog and sits above every plan's allowance, so\n quoting it advertises room the server will not honor.\n */}\n <QuotaReadoutComponent\n ready={org != null}\n used={formsUsed}\n limit={Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit}\n noun=\"form\"\n />\n {/*\n A retired form keeps its slot, and the readout says so rather than\n leaving a merchant to work out why a catalog of three is using five\n (AGL-2674). The count comes from the same aggregate\n `/api/hosts/resources` enforces on, which subtracts deleted forms\n and nothing else — so retirement is deliberately not a way to buy\n room back, and the number here is the number the server will\n refuse a create at.\n\n Shown only when a retirement is actually in the loaded page: the\n line answers a question nobody is asking on a site that has never\n retired anything. It makes no numeric claim, because one page is a\n lower bound on how many there are.\n */}\n {formWindow.some((form: any) => isFormArchived(form)) ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Retired forms keep their slot'}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n {/*\n Other ways to start a form, from plugins (AGL-3043): the\n `hostForms` zone, drawn through the shell's own gated slot. A\n plugin page cannot mount that slot itself, so the shell hands it\n down, and a widget here passes the gates a console page's would.\n */}\n {CreateZone ? (\n <CreateZone slot=\"hostForms\" hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={creating}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {creating ? 'Creating…' : 'Create Form'}\n </Button>\n </Stack>\n </Stack>\n </PageHeaderActions>\n <CardDisplay\n header=\"Forms\"\n help={pluginDocsHelp('forms', {\n anchor: '#find-a-form',\n excerpt:\n 'A form collects submissions into the Inbox, and its design is ' +\n 'drawn in the besigner like any other artifact.',\n })}\n >\n {retireError ? (\n <Alert severity=\"error\" sx={{ mb: 2 }}>\n {retireError}\n </Alert>\n ) : null}\n <ListFilterChips {...formFilter.chipsProps} />\n {formFilter.filtering && hasMore ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Filtering the ${formFilter.read} forms read so far — the next page reads more.`}\n </Typography>\n ) : null}\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={formFilter.filterColumns(columns)}\n initialState={{ columns: { columnVisibilityModel: FORM_HIDDEN_COLUMNS } }}\n // The panel and the search are the grid's; the card answers them\n // over what its window read.\n {...formFilter.gridProps}\n {...(formFilter.filtering\n ? { noRowsLabel: 'No forms match these filters' }\n : {\n noRowsLabel: 'No forms yet',\n noRowsDescription:\n 'A form collects submissions, dedupes the people who send them, and can route them to a lead. Its design is drawn in the besigner and published like any other artifact.',\n noRowsAction: (\n <Button variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'Create your first form'}\n </Button>\n ),\n })}\n rows={forms}\n // The whole row opens the detail page; the action cluster stops\n // propagation so a menu click never navigates underneath it.\n onOpen={(id) => router.push(formHref(String(id)))}\n // An empty table while the read is in flight reads as \"you have none\"\n // rather than \"these are on their way\".\n loading={status === 'loading'}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n <ListPagination {...formFilter.pagination} />\n {/*\n The console's own create drawer, from the shared library rather than a\n second one that looks like it. The empty state and the header open the\n SAME one, so the state that says \"creating…\" is one state.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new form\"\n // A form document stores no description: `/api/hosts/resources` filters\n // `data` through a per-kind allow-list, so one typed here is dropped\n // without a word.\n includeDescription={false}\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {createError}\n </Alert>\n ) : null\n }\n />\n </CardDisplay>\n </>\n )\n}\nHostFormsCard.displayName = 'HostFormsCard'\n\nexport default HostFormsCard\n"],"names":["Aglyn","buildRoute","isFormArchived","PageHeaderActions","pluginDocsHelp","Route","useConsoleWidgetSlot","ICON_VARIANT_SHOW_DETAIL","mdiArchiveArrowDownOutline","mdiArchiveArrowUpOutline","mdiContentCopy","mdiEyeOutline","mdiVectorSquare","AppLink","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListTable","ListRowActions","listActionsColumn","inMemoryListField","TABLE_ROW_HEIGHT","usePagedRowsFilter","QuotaReadoutComponent","CreateArtifactDrawer","Alert","Button","Stack","Typography","collection","doc","updateDoc","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useHostResourceApi","useLiveArtifactCount","usePagedCollection","collectionPage","useRouter","useCallback","useMemo","useState","BUNDLE_ID","FORM_FILTER_FIELDS","FORM_FILTER_HEADERS","displayName","slug","status","FORM_FILTER_OPTIONS","value","label","FORM_SEARCH_FIELDS","FORM_HIDDEN_COLUMNS","withStatusKey","form","statusKey","HostFormsCard","props","hostId","basePath","org","router","duplicate","onDuplicated","_kind","copy","push","formHref","id","orgSlug","subdomain","host","firestore","createHostResource","CreateZone","formId","FORM_DETAILS","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","retireError","setRetireError","setRetired","retired","archivedAt","Date","now","data","formData","rows","formWindow","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","formClauses","setFormClauses","askedStatus","some","clause","field","formRows","filter","map","formWindowRows","formFilter","fields","options","headers","search","clauses","onChange","forms","liveFormCount","formsUsed","length","handleCreate","values","createResourceUid","resource","normalizeFormSlug","rootId","CANVAS_ROOT_ELEMENT_ID","nodes","$id","componentId","formRoot","pluginId","parentId","formName","error","console","columns","headerName","minWidth","type","renderCell","href","flex","valueFormatter","align","headerAlign","valueGetter","_value","row","stats","submissions","toLocaleString","leads","lastSubmissionAtMs","toDate","versionId","quick","icon","path","to","FORM_PREVIEW","unavailableReason","items","key","size","FORM_BESIGNER","onClick","request","name","dialog","direction","spacing","sx","alignItems","ready","used","limit","checkQuota","noun","variant","color","slot","orgId","disabled","header","help","anchor","excerpt","severity","mb","chipsProps","filtering","read","rowHeight","filterColumns","initialState","columnVisibilityModel","gridProps","noRowsLabel","noRowsDescription","noRowsAction","onOpen","String","loading","hideFooter","pagination","open","onClose","title","includeDescription","onSubmit","errorSlot","mt"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,YAAYA,WAAW,eAAc;AAErC,SACEC,UAAU,EACVC,cAAc,EACdC,iBAAiB,EACjBC,cAAc,EACdC,KAAK,QACA,eAAc;AACrB,SAASC,oBAAoB,QAAQ,qDAAoD;AACzF,SAASC,wBAAwB,QAAQ,2BAA0B;AACnE,SACEC,0BAA0B,EAC1BC,wBAAwB,EACxBC,cAAc,EACdC,aAAa,EACbC,eAAe,QACV,yBAAwB;AAC/B,SAASC,OAAO,EAAEC,WAAW,EAAEC,OAAO,QAAQ,uBAAsB;AACpE,OAAOC,qBAAqB,8DAA6D;AACzF,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,OAAOC,aACLC,cAAc,EACdC,iBAAiB,QACZ,uDAAsD;AAC7D,SACEC,iBAAiB,QAEZ,8CAA6C;AACpD,SAASC,gBAAgB,QAAQ,8CAA6C;AAC9E,SAASC,kBAAkB,QAAQ,mDAAkD;AACrF,OAAOC,2BAA2B,0DAAyD;AAC3F,SAASC,oBAAoB,QAAQ,6BAA4B;AACjE,SAASC,KAAK,EAAEC,MAAM,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAEhE,SAASC,UAAU,EAAEC,GAAG,EAAEC,SAAS,QAAQ,qBAAoB;AAC/D,SACEC,oBAAoB,EACpBC,mBAAmB,EACnBC,oBAAoB,EACpBC,YAAY,EACZC,kBAAkB,EAClBC,oBAAoB,EACpBC,kBAAkB,QACb,iCAAgC;AACvC,SAASC,cAAc,QAAQ,+DAA8D;AAC7F,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,WAAW,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACtD,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;;CAKC,GACD,MAAMC,qBAAqB;IACzBzB,kBAAkB,eAAe;IACjCA,kBAAkB,QAAQ;IAC1BA,kBAAkB,UAAU,UAAU;CACvC;AACD,MAAM0B,sBAAwD;IAC5DC,aAAa;IACbC,MAAM;IACNC,QAAQ;AACV;AACA,MAAMC,sBAAsB;IAC1BD,QAAQ;QACN;YAAEE,OAAO;YAAUC,OAAO;QAAS;QACnC;YAAED,OAAO;YAAWC,OAAO;QAAU;KACtC;AACH;AACA,MAAMC,qBAAqB;IAAC;IAAe;CAAO;AAClD,MAAMC,sBAAsB;IAAEL,QAAQ;AAAM;AAE5C,sDAAsD,GACtD,MAAMM,gBAAgB,CAACC,OAAe,aACjCA;QACHC,WAAWxD,eAAeuD,QAAQ,YAAY;;AA0BhD;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,SAASE,cAAcC,KAAyB;IACrD,MAAM,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,GAAG,EAAE,GAAGH;IAClC,MAAMI,SAASvB;IACf,wEAAwE;IACxE,wDAAwD;IACxD,MAAMwB,YAAY9B,qBAAqB;QACrC0B;QACAK,cAAc,CAACC,OAAOC,OAASJ,OAAOK,IAAI,CAACC,SAASF,KAAKG,EAAE;IAC7D;IACA,MAAM,EAAEC,OAAO,EAAEC,WAAWC,IAAI,EAAE,GAAGxC,oBAAoB2B;IACzD,MAAMc,YAAYvC;IAClB,MAAMwC,qBAAqBvC;IAC3B;;;GAGC,GACD,MAAMwC,aAAavE;IAEnB;;;;;;GAMC,GACD,MAAMgE,WAAW5B,YACf,CAACoC,SACChB,WACI,GAAGA,SAAS,CAAC,EAAEgB,QAAQ,GACvB7E,WAAWI,MAAM0E,YAAY,EAAE;YAC7BP,OAAO,EAAEA,kBAAAA,UAAW;YACpBE,IAAI,EAAEA,eAAAA,OAAQ;YACdI;QACF,IACN;QAAChB;QAAUU;QAASE;KAAK;IAG3B,MAAM,CAACM,YAAYC,cAAc,GAAGrC,SAAS;IAC7C,MAAM,CAACsC,aAAaC,eAAe,GAAGvC,SAAwB;IAC9D,MAAM,CAACwC,UAAUC,YAAY,GAAGzC,SAAS;IACzC,MAAM,CAAC0C,aAAaC,eAAe,GAAG3C,SAAwB;IAE9D;;;;;;;;;;;;;;;;GAgBC,GACD,MAAM4C,aAAa9C,YACjB,OAAOoC,QAAgBW;QACrBF,eAAe;QACf,IAAI;YACF,MAAMvD,UAAUD,IAAI4C,WAAW,SAASd,QAAQ,SAASiB,SAAS;gBAChEY,YAAYD,UAAUE,KAAKC,GAAG,KAAK;YACrC;QACF,EAAE,eAAM;YACNL,eACEE,UACI,mDACA;QAER;IACF,GACA;QAACd;QAAWd;KAAO;IAGrB;;;;;;;;;;;GAWC,GACD,MAAM,EACJX,MAAM,EACN2C,MAAMC,QAAQ,EACdC,MAAMC,UAAU,EAChBC,OAAO,EACPC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACZ,GAAG9D,mBACF,CAAC+D,YACC9D,eACEV,WAAW6C,WAAW,SAASd,QAAQ,UACvCyC,YAEJ;QAAC3B;QAAWd;KAAO,EACnB;QAAE0C,SAAS;IAAM;IAEnB;;;;;;;;;;;;;;;;;GAiBC,GACD,MAAM,CAACC,aAAaC,eAAe,GAAG7D,SAA6B,EAAE;IACrE,MAAM8D,cAAcF,YAAYG,IAAI,CAAC,CAACC,SAAWA,OAAOC,KAAK,KAAK;IAClE,MAAMC,WAAWnE,QACf,IACEqD,WACGe,MAAM,CAAC,CAACtD,OAAciD,eAAe,CAACxG,eAAeuD,OACrDuD,GAAG,CAACxD,gBACT;QAACwC;QAAYU;KAAY;IAE3B,MAAMO,iBAAiBtE,QACrB,IACEmD,4BAAAA,SACIiB,MAAM,CAAC,CAACtD,OAAciD,eAAe,CAACxG,eAAeuD,OACtDuD,GAAG,CAACxD,gBACT;QAACsC;QAAUY;KAAY;IAEzB,MAAMQ,aAAa3F,mBACjB;QACEsE,MAAMoB;QACNlB,MAAMe;QACNb;QACAC;QACAC;QACAC;QACAC;IACF,GACA;QACEc,QAAQrE;QACRsE,SAASjE;QACTkE,SAAStE;QACTuE,QAAQhE;QACRiE,SAASf;QACTgB,UAAUf;IACZ;IAEF,4DAA4D;IAC5D,MAAMgB,QAAQP,WAAWnB,IAAI;IAE7B;;;;GAIC,GACD,MAAM2B,gBAAgBpF,qBAAqBuB,QAAQ;IACnD,wEAAwE;IACxE,kBAAkB;IAClB,MAAM8D,YAAYD,wBAAAA,gBAAiB1B,WAAW4B,MAAM;IAEpD;;;;;;;;GAQC,GACD,MAAMC,eAAenF,YACnB,OAAOoF;QACL,IAAI1C,UAAU;QACdC,YAAY;QACZF,eAAe;QACf,IAAI;YACF,MAAML,SAAS9E,MAAM+H,iBAAiB;YACtC,MAAMnD,mBAAmB;gBACvBf;gBACAmE,UAAU;gBACVzD,IAAIO;gBACJe,MAAM;oBACJ7C,aAAa8E,MAAM,CAAC,cAAc;oBAClC7E,MAAMjD,MAAMiI,iBAAiB,CAACH,MAAM,CAAC,cAAc,KAAKhD;oBACxDqC,QAAQ,EAAE;oBACVe,QAAQlI,MAAMmI,sBAAsB;oBACpCC,OAAO;wBACL,CAACpI,MAAMmI,sBAAsB,CAAC,EAAE;4BAC9BE,KAAKrI,MAAMmI,sBAAsB;4BACjCG,aAAa;4BACbF,OAAO;gCAAC;6BAAW;wBACrB;wBACAG,UAAU;4BACRF,KAAK;4BACLC,aAAa;4BACbE,UAAU3F;4BACV4F,UAAUzI,MAAMmI,sBAAsB;4BACtCvE,OAAO;gCAAEkB;gCAAQ4D,UAAUZ,MAAM,CAAC,cAAc;4BAAC;4BACjDM,OAAO,EAAE;wBACX;oBACF;gBACF;YACF;YACAnD,cAAc;YACdjB,OAAOK,IAAI,CAACC,SAASQ;QACvB,EAAE,OAAO6D,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdxD,eAAe;QACjB,SAAU;YACRE,YAAY;QACd;IACF,GACA;QAACD;QAAUR;QAAoBf;QAAQG;QAAQM;KAAS;IAG1D,MAAMuE,UAAwB;QAC5B;YACEhC,OAAO;YACPiC,YAAY;YACZC,UAAU;YACVC,MAAM;YACNC,YAAY,CAAC,EAAE1E,EAAE,EAAEnB,KAAK,EAAO,iBAC7B,KAACvC;oBAAQqI,MAAM5E,SAASC;8BACrBnB,SAAUmB;;QAGjB;QACA;YACEsC,OAAO;YACPiC,YAAY;YACZC,UAAU;YACVI,MAAM;YACNH,MAAM;YACN,gEAAgE;YAChEI,gBAAgB,CAAChG,QAAeA,SAAS;QAC3C;QACA;YACEyD,OAAO;YACPiC,YAAY;YACZC,UAAU;YACVC,MAAM;YACN,sEAAsE;YACtE,sEAAsE;YACtE,qDAAqD;YACrDK,OAAO;YACPC,aAAa;YACbC,aAAa,CAACC,QAAaC;;oBAAaA;+BAAAA,wBAAAA,aAAAA,IAAKC,KAAK,qBAAVD,WAAYE,WAAW,mBAAI;;YACnEP,gBAAgB,CAAChG,QACf,OAAOA,UAAU,WAAWA,MAAMwG,cAAc,KAAK;QACzD;QACA;YACE/C,OAAO;YACPiC,YAAY;YACZC,UAAU;YACVC,MAAM;YACNK,OAAO;YACPC,aAAa;YACb;;;;;;;OAOC,GACDC,aAAa,CAACC,QAAaC;oBAClBA;uBAAP,QAAOA,wBAAAA,aAAAA,IAAKC,KAAK,qBAAVD,WAAYI,KAAK,MAAK,WAAWJ,IAAIC,KAAK,CAACG,KAAK,GAAG;;YAC5DT,gBAAgB,CAAChG,QACf,OAAOA,UAAU,WAAWA,MAAMwG,cAAc,KAAK;QACzD;QACA;YACE/C,OAAO;YACPiC,YAAY;YACZC,UAAU;YACVI,MAAM;YACNH,MAAM;YACNO,aAAa,CAACC,QAAaC;oBAClBA;uBAAP,QAAOA,wBAAAA,aAAAA,IAAKC,KAAK,qBAAVD,WAAYK,kBAAkB,MAAK,WACtC,IAAInE,KAAK8D,IAAIC,KAAK,CAACI,kBAAkB,IACrC;;YACNV,gBAAgB,CAAChG;oBAAeA;uBAAAA,CAAAA,0BAAAA,wBAAAA,MAAOwG,cAAc,qBAArBxG,2BAAAA,WAA6B;;QAC/D;QACA;YACEyD,OAAO;YACPiC,YAAY;YACZC,UAAU;YACVI,MAAM;YACNH,MAAM;YACN,sEAAsE;YACtE,gEAAgE;YAChEO,aAAa,CAACnG;;oBAAeA;+BAAAA,0BAAAA,gBAAAA,MAAO2G,MAAM,qBAAb3G,mBAAAA,yBAAqB;;YAClDgG,gBAAgB,CAAChG;oBAAeA;uBAAAA,CAAAA,0BAAAA,wBAAAA,MAAOwG,cAAc,qBAArBxG,2BAAAA,WAA6B;;QAC/D;QACAhC,kBAAkB,CAACqI;gBAKNhG;YAJX,MAAMA,OAAO,aAAKgG;gBAAKpB,KAAKoB,IAAIpB,GAAG;;YACnC,MAAM2B,YAAYvG,KAAKuG,SAAS;YAChC,qBACE,KAAC7I;gBACCkC,KAAK,GAAEI,oBAAAA,KAAKT,WAAW,YAAhBS,oBAAoBA,KAAK4E,GAAG;gBACnC4B,OAAO;oBACLC,MAAMvJ,cAAcwJ,IAAI;oBACxB9G,OAAO;mBAIH2G,aAAaxF,WAAWE,OACxB;oBACE0F,IAAInK,WAAWI,MAAMgK,YAAY,EAAE;wBACjC7F;wBACAE;wBACAI,QAAQrB,KAAK4E,GAAG;wBAChB2B;oBACF;gBACF,IACA;oBACEM,mBAAmBN,YACf,mCACA;gBACN;gBAENO,OAAO;oBACL;wBACEC,KAAK;wBACLnH,OAAO;wBACP6G,oBAAM,KAACnJ;4BAAQoJ,MAAM5J,yBAAyB4J,IAAI;4BAAEM,MAAM;;wBAC1DvB,MAAM5E,SAASb,KAAK4E,GAAG;oBACzB;oBACA;wBACEmC,KAAK;wBACLnH,OAAO;wBACP6G,oBAAM,KAACnJ;4BAAQoJ,MAAMvJ,gBAAgBuJ,IAAI;4BAAEM,MAAM;;wBACjD;;;;;;;eAOC,GACDvB,MACEc,aAAaxF,WAAWE,OACpBzE,WAAWI,MAAMqK,aAAa,EAAE;4BAC9BlG;4BACAE;4BACAI,QAAQrB,KAAK4E,GAAG;4BAChB2B;wBACF,KACA1F,SAASb,KAAK4E,GAAG;oBACzB;oBACA;;;;;;;;;aASC,GACD;wBACEmC,KAAK;wBACLnH,OAAOpB;wBACPiI,oBAAM,KAACnJ;4BAAQoJ,MAAMzJ,eAAeyJ,IAAI;4BAAEM,MAAM;;wBAChDE,SAAS;gCAGClH;mCAFRQ,UAAU2G,OAAO,CAAC,QAAQ;gCACxBrG,IAAId,KAAK4E,GAAG;gCACZwC,IAAI,GAAEpH,oBAAAA,KAAKT,WAAW,YAAhBS,oBAAoB;4BAC5B;;oBACJ;oBACAvD,eAAeuD,QACX;wBACE+G,KAAK;wBACLnH,OAAO;wBACP6G,oBAAM,KAACnJ;4BAAQoJ,MAAM1J,yBAAyB0J,IAAI;4BAAEM,MAAM;;wBAC1DE,SAAS,IAAM,KAAKnF,WAAW/B,KAAK4E,GAAG,EAAE;oBAC3C,IACA;wBACEmC,KAAK;wBACLnH,OAAO;wBACP6G,oBAAM,KAACnJ;4BAAQoJ,MAAM3J,2BAA2B2J,IAAI;4BAAEM,MAAM;;wBAC5DE,SAAS,IAAM,KAAKnF,WAAW/B,KAAK4E,GAAG,EAAE;oBAC3C;iBACL;;QAGP;KACD;IAED,qBACE;;YACGpE,UAAU6G,MAAM;0BAajB,KAAC3K;0BACC,cAAA,MAACyB;oBAAMmJ,WAAU;oBAAMC,SAAS;oBAAGC,IAAI;wBAAEC,YAAY;oBAAS;;sCAS5D,KAAC1J;4BACC2J,OAAOpH,OAAO;4BACdqH,MAAMzD;4BACN0D,OAAOrL,MAAMsL,UAAU,CAACvH,KAAK,gBAAgB4D,WAAW0D,KAAK;4BAC7DE,MAAK;;wBAgBNvF,WAAWW,IAAI,CAAC,CAAClD,OAAcvD,eAAeuD,uBAC7C,KAAC5B;4BAAW2J,SAAQ;4BAAUC,OAAM;sCACjC;6BAED;sCACJ,MAAC7J;4BAAMmJ,WAAU;4BAAMC,SAAS;;gCAO7BnG,2BACC,KAACA;oCAAW6G,MAAK;oCAAY7H,QAAQA;oCAAQ8H,KAAK,EAAE5H,uBAAAA,IAAKsE,GAAG;qCAC1D;8CACJ,KAAC1G;oCACC8I,MAAK;oCACLe,SAAQ;oCACRI,UAAUxG;oCACVuF,SAAS;wCACPxF,eAAe;wCACfF,cAAc;oCAChB;8CAECG,WAAW,cAAc;;;;;;;0BAKlC,MAACtE;gBACC+K,QAAO;gBACPC,MAAM1L,eAAe,SAAS;oBAC5B2L,QAAQ;oBACRC,SACE,mEACA;gBACJ;;oBAEC1G,4BACC,KAAC5D;wBAAMuK,UAAS;wBAAQhB,IAAI;4BAAEiB,IAAI;wBAAE;kCACjC5G;yBAED;kCACJ,KAACtE,8BAAoBkG,WAAWiF,UAAU;oBACzCjF,WAAWkF,SAAS,IAAInG,wBACvB,KAACpE;wBAAW2J,SAAQ;wBAAUC,OAAM;kCACjC,CAAC,cAAc,EAAEvE,WAAWmF,IAAI,CAAC,8CAA8C,CAAC;yBAEjF;kCACJ,KAACnL;wBACCoL,WAAWhL;wBACXuH,SAAS3B,WAAWqF,aAAa,CAAC1D;wBAClC2D,cAAc;4BAAE3D,SAAS;gCAAE4D,uBAAuBlJ;4BAAoB;wBAAE;uBAGpE2D,WAAWwF,SAAS,EACnBxF,WAAWkF,SAAS,GACrB;wBAAEO,aAAa;oBAA+B,IAC9C;wBACEA,aAAa;wBACbC,mBACE;wBACFC,4BACE,KAAClL;4BAAO6J,SAAQ;4BAAYb,SAAS,IAAM1F,cAAc;sCACtD;;oBAGP;wBACJc,MAAM0B;wBACN,gEAAgE;wBAChE,6DAA6D;wBAC7DqF,QAAQ,CAACvI,KAAOP,OAAOK,IAAI,CAACC,SAASyI,OAAOxI;wBAC5C,sEAAsE;wBACtE,wCAAwC;wBACxCyI,SAAS9J,WAAW;wBACpB,8DAA8D;wBAC9D+J,UAAU;;kCAEZ,KAAChM,6BAAmBiG,WAAWgG,UAAU;kCAMzC,KAACzL;wBACC0L,MAAMnI;wBACNoI,SAAS,IAAMnI,cAAc;wBAC7BoI,OAAM;wBACN,wEAAwE;wBACxE,qEAAqE;wBACrE,kBAAkB;wBAClBC,oBAAoB;wBACpBC,UAAU1F;wBACV2F,WACEtI,4BACE,KAACxD;4BAAMuK,UAAS;4BAAQhB,IAAI;gCAAEwC,IAAI;gCAAGvB,IAAI;4BAAE;sCACxChH;6BAED;;;;;;AAMhB;AACAvB,cAAcX,WAAW,GAAG;AAE5B,eAAeW,cAAa"}