@aglyn/plugins-forms 1.0.0-beta.143

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.
Files changed (45) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +35 -0
  3. package/package.json +52 -0
  4. package/src/index.d.ts +18 -0
  5. package/src/index.js +19 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/form-design-preview.component.d.ts +68 -0
  8. package/src/lib/components/form-design-preview.component.js +238 -0
  9. package/src/lib/components/form-design-preview.component.js.map +1 -0
  10. package/src/lib/components/form-detail-card.d.ts +53 -0
  11. package/src/lib/components/form-detail-card.js +827 -0
  12. package/src/lib/components/form-detail-card.js.map +1 -0
  13. package/src/lib/components/form-metrics-card.component.d.ts +67 -0
  14. package/src/lib/components/form-metrics-card.component.js +298 -0
  15. package/src/lib/components/form-metrics-card.component.js.map +1 -0
  16. package/src/lib/components/form-submissions-card.component.d.ts +40 -0
  17. package/src/lib/components/form-submissions-card.component.js +104 -0
  18. package/src/lib/components/form-submissions-card.component.js.map +1 -0
  19. package/src/lib/components/form-zones.d.ts +52 -0
  20. package/src/lib/components/form-zones.js +20 -0
  21. package/src/lib/components/form-zones.js.map +1 -0
  22. package/src/lib/components/form.d.ts +186 -0
  23. package/src/lib/components/form.js +1014 -0
  24. package/src/lib/components/form.js.map +1 -0
  25. package/src/lib/components/forms-console-page.d.ts +22 -0
  26. package/src/lib/components/forms-console-page.js +55 -0
  27. package/src/lib/components/forms-console-page.js.map +1 -0
  28. package/src/lib/components/host-forms-card.component.d.ts +56 -0
  29. package/src/lib/components/host-forms-card.component.js +533 -0
  30. package/src/lib/components/host-forms-card.component.js.map +1 -0
  31. package/src/lib/components/use-form-promote-api.d.ts +48 -0
  32. package/src/lib/components/use-form-promote-api.js +63 -0
  33. package/src/lib/components/use-form-promote-api.js.map +1 -0
  34. package/src/lib/constants/bundle-common.d.ts +31 -0
  35. package/src/lib/constants/bundle-common.js +31 -0
  36. package/src/lib/constants/bundle-common.js.map +1 -0
  37. package/src/lib/plugin.d.ts +25 -0
  38. package/src/lib/plugin.js +76 -0
  39. package/src/lib/plugin.js.map +1 -0
  40. package/src/lib/site.d.ts +53 -0
  41. package/src/lib/site.js +83 -0
  42. package/src/lib/site.js.map +1 -0
  43. package/src/lib/utils/generate-preset-id.d.ts +25 -0
  44. package/src/lib/utils/generate-preset-id.js +30 -0
  45. package/src/lib/utils/generate-preset-id.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-design-preview.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'\n// Every character that could close an attribute or open a tag, escaped by the\n// one implementation rather than a fifth copy of it. Subpath, not the library\n// index — see the note there.\nimport { escapeHtml } from '@aglyn/shared-util-tools/escape-html'\nimport { Box, Stack, Typography } from '@mui/material'\nimport { useMemo } from 'react'\n\nexport interface FormDesignPreviewProps {\n formId: string\n /** The form document's PUBLISHED `nodes` — what a visitor is served today. */\n nodes: unknown\n /** Which field the document names as the marketing opt-in. */\n consentFieldName?: string\n /** The document has not arrived yet, as against having no design. */\n loading?: boolean\n}\n\n/**\n * The `form` node inside a published design.\n *\n * A form document's tree holds exactly one, because the document IS that form\n * — but the tree is a flat map, so the node has to be found rather than\n * assumed to be the root.\n */\nfunction findFormNodeId(nodes: Record<string, any> | null): string | undefined {\n return Object.keys(nodes ?? {}).find(\n (id) => nodes?.[id]?.componentId === 'form',\n )\n}\n\n/**\n * One field, as the browser will submit it.\n *\n * The `name` attribute is the whole point: it is the key `/api/forms/submit`\n * reads out of the `FormData`, and it is the coupling `checkFormContract`\n * guards. Rendering the control WITH its name is what makes this a preview of\n * the contract rather than a picture of some inputs.\n */\nfunction renderField(field: Aglyn.FormFieldDecl, consentFieldName?: string): string {\n const name = escapeHtml(field.fieldName)\n const label = escapeHtml(field.label || field.fieldName)\n const required = field.required ? ' required' : ''\n const isConsent = Boolean(consentFieldName) && field.fieldName === consentFieldName\n const badge = isConsent\n ? '<span class=\"badge\">marketing consent</span>'\n : field.required\n ? '<span class=\"badge\">required</span>'\n : ''\n const options = (field.options ?? []).map((option) => escapeHtml(option))\n\n let control: string\n switch (field.fieldType) {\n case 'textarea':\n control = `<textarea name=\"${name}\" rows=\"3\"${required}></textarea>`\n break\n case 'select':\n control = `<select name=\"${name}\"${required}>${options\n .map((option) => `<option value=\"${option}\">${option}</option>`)\n .join('')}</select>`\n break\n case 'radio':\n control = `<div class=\"choices\">${options\n .map(\n (option) =>\n `<label class=\"choice\"><input type=\"radio\" name=\"${name}\" value=\"${option}\"${required}> ${option}</label>`,\n )\n .join('')}</div>`\n break\n case 'checkbox':\n control = `<label class=\"choice\"><input type=\"checkbox\" name=\"${name}\" value=\"on\"${required}> ${label}</label>`\n break\n case 'rating':\n // The runtime submits a number; a range says so without pretending to be\n // the star control the site draws.\n control = `<input type=\"range\" name=\"${name}\" min=\"1\" max=\"5\"${required}>`\n break\n case 'email':\n control = `<input type=\"email\" name=\"${name}\"${required}>`\n break\n default:\n control = `<input type=\"text\" name=\"${name}\"${required}>`\n }\n\n return [\n '<div class=\"field\">',\n `<div class=\"label\">${label}${badge}</div>`,\n control,\n `<div class=\"key\">name=\"${name}\"</div>`,\n '</div>',\n ].join('')\n}\n\n/** The style block, kept out of the builder so the markup reads as markup. */\nconst PREVIEW_CSS = `\n :root { color-scheme: light; }\n body {\n margin: 0; padding: 16px;\n font: 14px/1.5 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n color: #1a1a1a; background: #ffffff;\n }\n form { display: grid; gap: 16px; max-width: 520px; }\n .field { display: grid; gap: 4px; }\n .label { font-weight: 600; display: flex; align-items: center; gap: 8px; }\n .badge {\n font-weight: 500; font-size: 11px; letter-spacing: .04em;\n text-transform: uppercase; padding: 1px 6px; border-radius: 999px;\n background: #eceff4; color: #55606e;\n }\n .key { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px; color: #6b7280; }\n input[type=text], input[type=email], textarea, select {\n width: 100%; box-sizing: border-box; padding: 8px;\n border: 1px solid #c8cdd4; border-radius: 6px; font: inherit;\n background: #ffffff; color: inherit;\n }\n .choices { display: grid; gap: 4px; }\n .choice { font-weight: 400; display: flex; align-items: center; gap: 6px; }\n button {\n justify-self: start; padding: 8px 16px; border-radius: 6px;\n border: 1px solid #c8cdd4; background: #f4f6f8; font: inherit;\n }\n .none { color: #6b7280; }\n`\n\n/**\n * Builds the standalone document the frame is given.\n *\n * Exported for the spec: the property that matters — that no author-supplied\n * string can become markup — is a property of this string, and asserting it\n * through a rendered component would be asserting it through React's escaping\n * as well as this function's.\n */\nexport function buildFormPreviewDocument(options: {\n fields: Aglyn.FormFieldDecl[]\n consentFieldName?: string\n}): string {\n const { fields, consentFieldName } = options\n const body = fields.length\n ? fields.map((field) => renderField(field, consentFieldName)).join('')\n : '<p class=\"none\">This design declares no named fields.</p>'\n return [\n '<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">',\n '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">',\n `<style>${PREVIEW_CSS}</style></head><body>`,\n // `action` is deliberately absent and the frame is sandboxed, so this\n // cannot post anywhere even if a reader hits Return in a text field.\n `<form>${body}<button type=\"button\">Submit</button></form>`,\n '</body></html>',\n ].join('')\n}\n\n/**\n * THE FORM AS THE SUBMIT ROUTE WILL READ IT, DRAWN SAFELY.\n *\n * ## What is being previewed\n *\n * A form has two halves and they fail differently. The DESIGN — fonts,\n * spacing, the theme the site draws it in — is previewed by\n * `Route.FORM_PREVIEW`, which renders the stored nodes through\n * `AglynNodeRenderer`, the same component tree the published page mounts. That\n * is a whole-page surface and it stays where it is.\n *\n * This is the other half: the CONTRACT. `/api/forms/submit` never sees a\n * pixel. It sees `FormData` keys, and every coupling it depends on — which\n * field is the address a lead is created from, which field is the marketing\n * opt-in — is resolved by NAME. So the question this frame answers is the one\n * an author cannot answer by looking at the canvas: what names will arrive,\n * and which of them carry meaning.\n *\n * The field list is NOT re-derived here. `formFieldDeclsFromNodes` is the same\n * function the publish path calls to write the document's `fields` and the\n * same one adoption uses, including its two rules that are easy to get wrong\n * — an unnamed field is dropped, and a duplicate name keeps its first\n * occurrence — so this frame drops and keeps exactly what a real submission\n * does. A second implementation would be a preview of something else.\n *\n * ## Sandboxed, because every string in it is tenant-authored\n *\n * Labels, option text and field names are typed by a site's own editors, or\n * arrive on a marketplace template. They are escaped on the way into the\n * document AND the document is rendered into an iframe with an EMPTY `sandbox`\n * attribute — the maximally restrictive form: no scripts, no forms, no popups,\n * no top-level navigation and, critically, no same-origin. The frame gets an\n * opaque origin, so nothing inside it can reach the console's cookies, storage\n * or DOM. `srcDoc` rather than a URL keeps the markup from ever being served\n * from the console's own origin, where the sandbox attribute would be the only\n * thing between tenant content and a live session.\n *\n * The escaping and the sandbox are not redundant: the escaping is what keeps\n * the preview a preview of the author's text rather than of their markup, and\n * the sandbox is what makes being wrong about the escaping survivable.\n */\nexport function FormDesignPreview(props: FormDesignPreviewProps) {\n const { formId, nodes: rawNodes, consentFieldName, loading } = props\n\n const fields = useMemo(() => {\n const decoded = Aglyn.decodeStoredNodes<Record<string, any>>(rawNodes)\n if (!decoded || !Object.keys(decoded).length) return null\n const formNodeId = findFormNodeId(decoded)\n if (!formNodeId) return null\n return Aglyn.formFieldDeclsFromNodes(\n decoded as never,\n formNodeId as Aglyn.NodeId,\n )\n }, [rawNodes])\n\n const previewDocument = useMemo(\n () =>\n fields\n ? buildFormPreviewDocument({ fields, consentFieldName })\n : null,\n [fields, consentFieldName],\n )\n\n if (!previewDocument) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {loading\n ? 'Loading this form…'\n : 'Nothing published yet — open this form in the besigner, then ' +\n 'publish a version to see what it will collect.'}\n </Typography>\n )\n }\n\n return (\n <Stack spacing={1}>\n <Box\n component=\"iframe\"\n title={`Preview of form ${formId}`}\n /*\n * EMPTY sandbox — every restriction on, nothing allowed back. The\n * strings below are authored outside this console, so the document is\n * rendered with no scripts, no form submission, no navigation and an\n * opaque origin: it cannot reach the console's session, storage or DOM\n * even though it is drawn inside the console.\n */\n sandbox=\"\"\n referrerPolicy=\"no-referrer\"\n srcDoc={previewDocument}\n sx={{\n width: '100%',\n // Fixed, with the frame's own scrollbar. A sandbox with no scripts\n // is a frame that cannot measure or report its content height, and\n // an iframe cannot size to its content on its own.\n height: 420,\n border: 1,\n borderColor: 'divider',\n borderRadius: 1,\n bgcolor: 'common.white',\n }}\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'These are the names a submission arrives under, read off the ' +\n 'published design by the same function that writes the form’s ' +\n 'declared fields. The site’s own styling is not applied here — ' +\n 'use Preview for that.'}\n </Typography>\n </Stack>\n )\n}\nFormDesignPreview.displayName = 'FormDesignPreview'\n\nexport default FormDesignPreview\n"],"names":["Aglyn","escapeHtml","Box","Stack","Typography","useMemo","findFormNodeId","nodes","Object","keys","find","id","componentId","renderField","field","consentFieldName","name","fieldName","label","required","isConsent","Boolean","badge","options","map","option","control","fieldType","join","PREVIEW_CSS","buildFormPreviewDocument","fields","body","length","FormDesignPreview","props","formId","rawNodes","loading","decoded","decodeStoredNodes","formNodeId","formFieldDeclsFromNodes","previewDocument","variant","color","spacing","component","title","sandbox","referrerPolicy","srcDoc","sx","width","height","border","borderColor","borderRadius","bgcolor","displayName"],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ export interface FormDetailCardProps {
2
+ hostId: string;
3
+ formId: string;
4
+ /**
5
+ * The Forms surface's own absolute console path.
6
+ *
7
+ * Only the not-found branch links it. The trail carries the way back on a
8
+ * form that exists, so a link beside the heading would be the breadcrumb
9
+ * written twice; a form that does NOT exist has no heading of its own for
10
+ * the trail to end on, and the reader needs somewhere to go.
11
+ */
12
+ basePath?: string;
13
+ /** Whether this viewer's role on the site may make a version live. */
14
+ canPublish?: boolean;
15
+ /** False while the role is still being read; see the disabled reasons. */
16
+ hostRoleLoaded?: boolean;
17
+ }
18
+ /**
19
+ * One form — the component detail surface, for a document that is also a
20
+ * contract.
21
+ *
22
+ * The split this enforces is the reason it exists rather than sending a row
23
+ * straight into the besigner. A form has two halves:
24
+ *
25
+ * - the DESIGN, which is what an author draws, and which lives in the
26
+ * besigner reached from here;
27
+ * - the DECLARATION — where a submission is routed and which field carries
28
+ * marketing consent — which is edited here.
29
+ *
30
+ * They are edited apart because the declaration is what the design is then
31
+ * checked AGAINST. `checkFormContract` compares the two at publish, so a
32
+ * surface that let an author change both in one motion would let them satisfy
33
+ * the check by moving whichever side happened to be easier — which is how a
34
+ * form ends up with lead routing pointed at a field nobody fills in.
35
+ *
36
+ * ## Promotion lives here, not only in the besigner
37
+ *
38
+ * A component's version history offers Publish on any version: promotion is
39
+ * how you go back, not only how you go forward, and an author restoring last
40
+ * week's design should not have to open a canvas to do it.
41
+ *
42
+ * It rides `/api/hosts/forms/promote` rather than an `updateDoc` here, because
43
+ * a form's promotion has to run `checkFormContract` on the tree it is about to
44
+ * write and REFUSE. A check in this component would be advice a determined
45
+ * client could skip; the route reads the stored version itself, so nothing
46
+ * about the design crosses the wire inbound and there is no version of this
47
+ * surface that can publish a design the check has not seen.
48
+ */
49
+ export declare function FormDetailCard(props: FormDetailCardProps): import("react").JSX.Element;
50
+ export declare namespace FormDetailCard {
51
+ var displayName: string;
52
+ }
53
+ export default FormDetailCard;