@businessdash/sdk 0.9.60

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 (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +899 -0
  3. package/dist/adapters-revalidate.d.ts +44 -0
  4. package/dist/adapters-revalidate.d.ts.map +1 -0
  5. package/dist/adapters-revalidate.js +68 -0
  6. package/dist/adapters-revalidate.js.map +1 -0
  7. package/dist/alpha.d.ts +21 -0
  8. package/dist/alpha.d.ts.map +1 -0
  9. package/dist/alpha.js +21 -0
  10. package/dist/alpha.js.map +1 -0
  11. package/dist/analytics-core.d.ts +34 -0
  12. package/dist/analytics-core.d.ts.map +1 -0
  13. package/dist/analytics-core.js +138 -0
  14. package/dist/analytics-core.js.map +1 -0
  15. package/dist/analytics-server.d.ts +54 -0
  16. package/dist/analytics-server.d.ts.map +1 -0
  17. package/dist/analytics-server.js +78 -0
  18. package/dist/analytics-server.js.map +1 -0
  19. package/dist/attribution-logo.d.ts +2 -0
  20. package/dist/attribution-logo.d.ts.map +1 -0
  21. package/dist/attribution-logo.js +5 -0
  22. package/dist/attribution-logo.js.map +1 -0
  23. package/dist/auth-handler.d.ts +92 -0
  24. package/dist/auth-handler.d.ts.map +1 -0
  25. package/dist/auth-handler.js +277 -0
  26. package/dist/auth-handler.js.map +1 -0
  27. package/dist/biab-forms.css +484 -0
  28. package/dist/cli.d.ts +46 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +1824 -0
  31. package/dist/cli.js.map +1 -0
  32. package/dist/client.d.ts +1681 -0
  33. package/dist/client.d.ts.map +1 -0
  34. package/dist/client.js +2845 -0
  35. package/dist/client.js.map +1 -0
  36. package/dist/collections.d.ts +13 -0
  37. package/dist/collections.d.ts.map +1 -0
  38. package/dist/collections.js +13 -0
  39. package/dist/collections.js.map +1 -0
  40. package/dist/contracts.d.ts +14609 -0
  41. package/dist/contracts.d.ts.map +1 -0
  42. package/dist/contracts.js +2428 -0
  43. package/dist/contracts.js.map +1 -0
  44. package/dist/data-model-schema.d.ts +962 -0
  45. package/dist/data-model-schema.d.ts.map +1 -0
  46. package/dist/data-model-schema.js +2295 -0
  47. package/dist/data-model-schema.js.map +1 -0
  48. package/dist/distribution.d.ts +38 -0
  49. package/dist/distribution.d.ts.map +1 -0
  50. package/dist/distribution.js +61 -0
  51. package/dist/distribution.js.map +1 -0
  52. package/dist/element.d.ts +121 -0
  53. package/dist/element.d.ts.map +1 -0
  54. package/dist/element.js +276 -0
  55. package/dist/element.js.map +1 -0
  56. package/dist/enums.d.ts +25 -0
  57. package/dist/enums.d.ts.map +1 -0
  58. package/dist/enums.js +38 -0
  59. package/dist/enums.js.map +1 -0
  60. package/dist/errors.d.ts +195 -0
  61. package/dist/errors.d.ts.map +1 -0
  62. package/dist/errors.js +188 -0
  63. package/dist/errors.js.map +1 -0
  64. package/dist/forms-core/autocomplete.d.ts +37 -0
  65. package/dist/forms-core/autocomplete.d.ts.map +1 -0
  66. package/dist/forms-core/autocomplete.js +82 -0
  67. package/dist/forms-core/autocomplete.js.map +1 -0
  68. package/dist/forms-core/business-function.d.ts +61 -0
  69. package/dist/forms-core/business-function.d.ts.map +1 -0
  70. package/dist/forms-core/business-function.js +149 -0
  71. package/dist/forms-core/business-function.js.map +1 -0
  72. package/dist/forms-core/calculation.d.ts +72 -0
  73. package/dist/forms-core/calculation.d.ts.map +1 -0
  74. package/dist/forms-core/calculation.js +103 -0
  75. package/dist/forms-core/calculation.js.map +1 -0
  76. package/dist/forms-core/controller.d.ts +249 -0
  77. package/dist/forms-core/controller.d.ts.map +1 -0
  78. package/dist/forms-core/controller.js +590 -0
  79. package/dist/forms-core/controller.js.map +1 -0
  80. package/dist/forms-core/elements.d.ts +44 -0
  81. package/dist/forms-core/elements.d.ts.map +1 -0
  82. package/dist/forms-core/elements.js +68 -0
  83. package/dist/forms-core/elements.js.map +1 -0
  84. package/dist/forms-core/email-domains.d.ts +10 -0
  85. package/dist/forms-core/email-domains.d.ts.map +1 -0
  86. package/dist/forms-core/email-domains.js +69 -0
  87. package/dist/forms-core/email-domains.js.map +1 -0
  88. package/dist/forms-core/file-rules.d.ts +51 -0
  89. package/dist/forms-core/file-rules.d.ts.map +1 -0
  90. package/dist/forms-core/file-rules.js +105 -0
  91. package/dist/forms-core/file-rules.js.map +1 -0
  92. package/dist/forms-core/formula.d.ts +114 -0
  93. package/dist/forms-core/formula.d.ts.map +1 -0
  94. package/dist/forms-core/formula.js +1170 -0
  95. package/dist/forms-core/formula.js.map +1 -0
  96. package/dist/forms-core/index.d.ts +24 -0
  97. package/dist/forms-core/index.d.ts.map +1 -0
  98. package/dist/forms-core/index.js +28 -0
  99. package/dist/forms-core/index.js.map +1 -0
  100. package/dist/forms-core/units.d.ts +49 -0
  101. package/dist/forms-core/units.d.ts.map +1 -0
  102. package/dist/forms-core/units.js +255 -0
  103. package/dist/forms-core/units.js.map +1 -0
  104. package/dist/forms-core/us-states.d.ts +14 -0
  105. package/dist/forms-core/us-states.d.ts.map +1 -0
  106. package/dist/forms-core/us-states.js +66 -0
  107. package/dist/forms-core/us-states.js.map +1 -0
  108. package/dist/forms-core/validation.d.ts +266 -0
  109. package/dist/forms-core/validation.d.ts.map +1 -0
  110. package/dist/forms-core/validation.js +850 -0
  111. package/dist/forms-core/validation.js.map +1 -0
  112. package/dist/forms.d.ts +19 -0
  113. package/dist/forms.d.ts.map +1 -0
  114. package/dist/forms.js +29 -0
  115. package/dist/forms.js.map +1 -0
  116. package/dist/gallery-types.d.ts +32 -0
  117. package/dist/gallery-types.d.ts.map +1 -0
  118. package/dist/gallery-types.js +2 -0
  119. package/dist/gallery-types.js.map +1 -0
  120. package/dist/graphql.d.ts +97 -0
  121. package/dist/graphql.d.ts.map +1 -0
  122. package/dist/graphql.js +212 -0
  123. package/dist/graphql.js.map +1 -0
  124. package/dist/index.cjs +19448 -0
  125. package/dist/index.d.cts +19 -0
  126. package/dist/index.d.mts +19 -0
  127. package/dist/index.d.ts +19 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +20 -0
  130. package/dist/index.js.map +1 -0
  131. package/dist/marketing-schema.d.ts +216 -0
  132. package/dist/marketing-schema.d.ts.map +1 -0
  133. package/dist/marketing-schema.js +200 -0
  134. package/dist/marketing-schema.js.map +1 -0
  135. package/dist/mcp.d.ts +42 -0
  136. package/dist/mcp.d.ts.map +1 -0
  137. package/dist/mcp.js +87 -0
  138. package/dist/mcp.js.map +1 -0
  139. package/dist/native.d.ts +45 -0
  140. package/dist/native.d.ts.map +1 -0
  141. package/dist/native.js +43 -0
  142. package/dist/native.js.map +1 -0
  143. package/dist/next-revalidate.d.ts +43 -0
  144. package/dist/next-revalidate.d.ts.map +1 -0
  145. package/dist/next-revalidate.js +84 -0
  146. package/dist/next-revalidate.js.map +1 -0
  147. package/dist/parallel-pages.d.ts +102 -0
  148. package/dist/parallel-pages.d.ts.map +1 -0
  149. package/dist/parallel-pages.js +108 -0
  150. package/dist/parallel-pages.js.map +1 -0
  151. package/dist/payment-icons.d.ts +66 -0
  152. package/dist/payment-icons.d.ts.map +1 -0
  153. package/dist/payment-icons.js +160 -0
  154. package/dist/payment-icons.js.map +1 -0
  155. package/dist/proxy.d.ts +67 -0
  156. package/dist/proxy.d.ts.map +1 -0
  157. package/dist/proxy.js +97 -0
  158. package/dist/proxy.js.map +1 -0
  159. package/dist/react-analytics.d.ts +39 -0
  160. package/dist/react-analytics.d.ts.map +1 -0
  161. package/dist/react-analytics.js +59 -0
  162. package/dist/react-analytics.js.map +1 -0
  163. package/dist/react-attribution.d.ts +99 -0
  164. package/dist/react-attribution.d.ts.map +1 -0
  165. package/dist/react-attribution.js +52 -0
  166. package/dist/react-attribution.js.map +1 -0
  167. package/dist/react-marketing.d.ts +181 -0
  168. package/dist/react-marketing.d.ts.map +1 -0
  169. package/dist/react-marketing.js +215 -0
  170. package/dist/react-marketing.js.map +1 -0
  171. package/dist/react.d.ts +727 -0
  172. package/dist/react.d.ts.map +1 -0
  173. package/dist/react.js +2418 -0
  174. package/dist/react.js.map +1 -0
  175. package/dist/reserved-paths.d.ts +24 -0
  176. package/dist/reserved-paths.d.ts.map +1 -0
  177. package/dist/reserved-paths.js +60 -0
  178. package/dist/reserved-paths.js.map +1 -0
  179. package/dist/revalidate-verify.d.ts +30 -0
  180. package/dist/revalidate-verify.d.ts.map +1 -0
  181. package/dist/revalidate-verify.js +74 -0
  182. package/dist/revalidate-verify.js.map +1 -0
  183. package/dist/sdk.d.ts +678 -0
  184. package/dist/sdk.d.ts.map +1 -0
  185. package/dist/sdk.js +801 -0
  186. package/dist/sdk.js.map +1 -0
  187. package/dist/seo.d.ts +218 -0
  188. package/dist/seo.d.ts.map +1 -0
  189. package/dist/seo.js +309 -0
  190. package/dist/seo.js.map +1 -0
  191. package/dist/socials.d.ts +47 -0
  192. package/dist/socials.d.ts.map +1 -0
  193. package/dist/socials.js +172 -0
  194. package/dist/socials.js.map +1 -0
  195. package/dist/static.d.ts +14 -0
  196. package/dist/static.d.ts.map +1 -0
  197. package/dist/static.js +14 -0
  198. package/dist/static.js.map +1 -0
  199. package/dist/url-pattern.d.ts +47 -0
  200. package/dist/url-pattern.d.ts.map +1 -0
  201. package/dist/url-pattern.js +124 -0
  202. package/dist/url-pattern.js.map +1 -0
  203. package/dist/vanilla.d.ts +77 -0
  204. package/dist/vanilla.d.ts.map +1 -0
  205. package/dist/vanilla.js +903 -0
  206. package/dist/vanilla.js.map +1 -0
  207. package/openapi.json +13779 -0
  208. package/package.json +204 -0
  209. package/src/adapters-revalidate.ts +88 -0
  210. package/src/alpha.ts +89 -0
  211. package/src/analytics-core.ts +175 -0
  212. package/src/analytics-server.ts +99 -0
  213. package/src/angular/biab-form.component.ts +1443 -0
  214. package/src/angular/index.ts +26 -0
  215. package/src/attribution-logo.ts +5 -0
  216. package/src/auth-handler.ts +352 -0
  217. package/src/cli.ts +2517 -0
  218. package/src/client.ts +3578 -0
  219. package/src/collections.ts +101 -0
  220. package/src/contracts.ts +3390 -0
  221. package/src/data-model-schema.ts +4042 -0
  222. package/src/distribution.ts +73 -0
  223. package/src/element.ts +321 -0
  224. package/src/enums.ts +65 -0
  225. package/src/errors.ts +338 -0
  226. package/src/forms-core/autocomplete.ts +97 -0
  227. package/src/forms-core/business-function.ts +209 -0
  228. package/src/forms-core/calculation.ts +167 -0
  229. package/src/forms-core/controller.ts +922 -0
  230. package/src/forms-core/elements.ts +86 -0
  231. package/src/forms-core/email-domains.ts +68 -0
  232. package/src/forms-core/file-rules.ts +123 -0
  233. package/src/forms-core/formula.ts +1339 -0
  234. package/src/forms-core/index.ts +122 -0
  235. package/src/forms-core/units.ts +308 -0
  236. package/src/forms-core/us-states.ts +68 -0
  237. package/src/forms-core/validation.ts +1037 -0
  238. package/src/forms.ts +63 -0
  239. package/src/gallery-types.ts +31 -0
  240. package/src/graphql.ts +295 -0
  241. package/src/index.ts +505 -0
  242. package/src/marketing-schema.ts +395 -0
  243. package/src/mcp.ts +108 -0
  244. package/src/native.ts +73 -0
  245. package/src/next-revalidate.ts +124 -0
  246. package/src/parallel-pages.ts +192 -0
  247. package/src/payment-icons.ts +205 -0
  248. package/src/proxy.ts +173 -0
  249. package/src/qwik.tsx +1234 -0
  250. package/src/react-analytics.tsx +76 -0
  251. package/src/react-attribution.tsx +192 -0
  252. package/src/react-marketing.tsx +338 -0
  253. package/src/react.tsx +5495 -0
  254. package/src/reserved-paths.ts +65 -0
  255. package/src/revalidate-verify.ts +98 -0
  256. package/src/sdk.ts +1671 -0
  257. package/src/seo.ts +488 -0
  258. package/src/socials.ts +230 -0
  259. package/src/solid.tsx +1466 -0
  260. package/src/static.ts +21 -0
  261. package/src/styles/biab-forms.css +484 -0
  262. package/src/svelte/BiabForm.svelte +1069 -0
  263. package/src/svelte/context.ts +57 -0
  264. package/src/svelte/index.ts +79 -0
  265. package/src/url-pattern.ts +153 -0
  266. package/src/vanilla.ts +1290 -0
  267. package/src/vue/BiabForm.vue +1538 -0
  268. package/src/vue/index.ts +119 -0
@@ -0,0 +1,903 @@
1
+ /**
2
+ * `@businessdash/sdk/vanilla` — framework-free imperative DOM binding for `<BiabForm>`.
3
+ *
4
+ * `mountBiabForm(elOrSelector, opts)` mounts a fully-rendered, schema-driven form
5
+ * into any DOM node. It owns nothing of the form's logic — all of that lives in
6
+ * the headless `createFormController` core (`@businessdash/sdk/forms`). This binding
7
+ * is the "render layer": it subscribes to the controller's `snapshot()` and, on
8
+ * every notify, rebuilds the form's DOM by walking `snapshot.visibleFields` and
9
+ * dispatching per field `type`. It is a faithful port of the dashboard renderer
10
+ * (`FormPreview.tsx`) so a drop-in `<biab-form>` reproduces the builder preview.
11
+ *
12
+ * Rendering rules (kept identical to the dashboard):
13
+ * - Light DOM, no shadow root, so the consumer's CSS cascades straight in.
14
+ * - Stable `biab-*` class hooks + `data-biab-*` attributes on every node.
15
+ * - Applies `schema.settings`: animation (none/fade/slide/scale/scroll),
16
+ * multi-step ("consecutive") nav, progress bar/steps, concurrent-reveal
17
+ * gating inside groups, plus the org icon when `showOrgIcon` is set.
18
+ * - condition_block / or_condition / input_group / flex_row layout nodes are
19
+ * walked exactly as the core's validation helpers expect.
20
+ *
21
+ * Compiled to dist (plain TS, no JSX). The web-component binding
22
+ * (`@businessdash/sdk/element`) wraps this same `mountBiabForm`.
23
+ */
24
+ import { resolveFieldAutocomplete, } from "./forms-core/autocomplete.js";
25
+ import { concurrentRevealCount, createFormController, evalCondition, } from "./forms-core/index.js";
26
+ // Re-export the core so consumers can reach the controller through this subpath.
27
+ export * from "./forms-core/index.js";
28
+ function el(tag, attrs = {}, children = []) {
29
+ const node = document.createElement(tag);
30
+ for (const [k, v] of Object.entries(attrs)) {
31
+ if (v == null || v === false)
32
+ continue;
33
+ if (k === "class")
34
+ node.className = String(v);
35
+ else if (v === true)
36
+ node.setAttribute(k, "");
37
+ else
38
+ node.setAttribute(k, String(v));
39
+ }
40
+ for (const child of children) {
41
+ if (child == null || child === false)
42
+ continue;
43
+ node.append(typeof child === "string" ? document.createTextNode(child) : child);
44
+ }
45
+ return node;
46
+ }
47
+ function classNames(...parts) {
48
+ return parts.filter(Boolean).join(" ");
49
+ }
50
+ /** Escape a value for use inside a `[data-field-id="…"]` attribute selector. */
51
+ function cssEscape(value) {
52
+ const cssApi = globalThis
53
+ .CSS;
54
+ if (cssApi && typeof cssApi.escape === "function")
55
+ return cssApi.escape(value);
56
+ // Field ids are UUIDs/slugs, but quote-escape defensively as a fallback.
57
+ return value.replace(/["\\]/g, "\\$&");
58
+ }
59
+ /** Stable, override-friendly CSS hook for a field (mirrors FormPreview.fieldCssKey).
60
+ * Prefers the published output `key`, else the field id. */
61
+ function fieldCssKey(field) {
62
+ const explicit = (typeof field.outputKey === "string" && field.outputKey.trim()) ||
63
+ (typeof field.key === "string" && field.key.trim());
64
+ return explicit || field.id;
65
+ }
66
+ const NO_LABEL_TYPES = new Set(["title", "subtitle", "separator", "richtext"]);
67
+ /** tw-style entrance class hook for the active step (consumer CSS animates these).
68
+ * Matches the `biab-anim--*` convention so orgs can target each animation. */
69
+ function animClass(animation, forward) {
70
+ switch (animation) {
71
+ case "fade":
72
+ return "biab-anim biab-anim--fade";
73
+ case "scale":
74
+ return "biab-anim biab-anim--scale";
75
+ case "slide":
76
+ return forward
77
+ ? "biab-anim biab-anim--slide biab-anim--slide-forward"
78
+ : "biab-anim biab-anim--slide biab-anim--slide-back";
79
+ default:
80
+ return "";
81
+ }
82
+ }
83
+ // ─────────────────────────────────────────────────────────────────────────────
84
+ // Per-field input rendering — dispatch by `type`, port of FormPreview.FieldInput.
85
+ // Each input writes back to the controller via `set(value)`.
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+ /** Set `autocomplete` only when a token is defined (omit the attribute on
88
+ * `undefined`, never emit `autocomplete=""`). Mirrors the React renderer. */
89
+ function setAutocomplete(el, token) {
90
+ if (token !== undefined)
91
+ el.setAttribute("autocomplete", token);
92
+ }
93
+ function renderFieldInput(field, snapshot, set, upload, autoComplete) {
94
+ const value = snapshot.values[field.id];
95
+ const str = typeof value === "string" ? value : "";
96
+ // Address composite suppresses its fixed sub-field tokens to "off" when the
97
+ // field's resolved token is "off" (global switch or per-field override).
98
+ const acOff = autoComplete === "off";
99
+ switch (field.type) {
100
+ case "title":
101
+ return el("h2", { class: "biab-field__title" }, [
102
+ field.label || "Untitled",
103
+ ]);
104
+ case "subtitle":
105
+ return el("h3", { class: "biab-field__subtitle" }, [field.label || ""]);
106
+ case "separator":
107
+ return el("hr", { class: "biab-field__separator" });
108
+ case "richtext": {
109
+ if (!field.content)
110
+ return null;
111
+ const div = el("div", { class: "biab-field__richtext biab-prose" });
112
+ // The published `content` is server-sanitized HTML.
113
+ div.innerHTML = field.content;
114
+ return div;
115
+ }
116
+ case "image": {
117
+ const cfg = (field["imageConfig"] ?? {});
118
+ if (!cfg.src)
119
+ return null;
120
+ return el("img", {
121
+ class: "biab-field__image",
122
+ src: cfg.src,
123
+ alt: cfg.alt || "",
124
+ ...(cfg.width ? { width: cfg.width } : {}),
125
+ ...(cfg.height ? { height: cfg.height } : {}),
126
+ });
127
+ }
128
+ case "textarea": {
129
+ const cfg = (field["textareaConfig"] ?? {});
130
+ const node = el("textarea", {
131
+ class: classNames("biab-textarea", cfg.resizable === false && "biab-textarea--fixed"),
132
+ rows: cfg.rows ?? 4,
133
+ placeholder: field.placeholder || "",
134
+ });
135
+ setAutocomplete(node, autoComplete);
136
+ node.value = str;
137
+ node.addEventListener("input", () => set(node.value));
138
+ return node;
139
+ }
140
+ case "select": {
141
+ const node = el("select", { class: "biab-select" });
142
+ node.append(el("option", { value: "" }, [field.placeholder || "Select..."]));
143
+ for (const opt of field.options ?? []) {
144
+ node.append(el("option", { value: opt }, [opt]));
145
+ }
146
+ node.value = str;
147
+ node.addEventListener("change", () => set(node.value));
148
+ return node;
149
+ }
150
+ case "radio": {
151
+ const wrap = el("div", { class: "biab-radio-group" });
152
+ for (const opt of field.options ?? []) {
153
+ const id = `${field.id}-${opt}`;
154
+ const input = el("input", {
155
+ class: "biab-radio",
156
+ type: "radio",
157
+ name: field.id,
158
+ id,
159
+ value: opt,
160
+ });
161
+ input.checked = str === opt;
162
+ input.addEventListener("change", () => set(opt));
163
+ wrap.append(el("label", { class: "biab-radio-option", for: id }, [
164
+ input,
165
+ el("span", {}, [opt]),
166
+ ]));
167
+ }
168
+ return wrap;
169
+ }
170
+ case "checkbox": {
171
+ const arr = Array.isArray(value) ? value : [];
172
+ const wrap = el("div", { class: "biab-checkbox-group" });
173
+ for (const opt of field.options ?? []) {
174
+ const input = el("input", {
175
+ class: "biab-checkbox",
176
+ type: "checkbox",
177
+ value: opt,
178
+ });
179
+ input.checked = arr.includes(opt);
180
+ input.addEventListener("change", () => {
181
+ const next = input.checked
182
+ ? [...arr, opt]
183
+ : arr.filter((x) => x !== opt);
184
+ set(next);
185
+ });
186
+ wrap.append(el("label", { class: "biab-checkbox-option" }, [
187
+ input,
188
+ el("span", {}, [opt]),
189
+ ]));
190
+ }
191
+ return wrap;
192
+ }
193
+ case "boolean": {
194
+ const input = el("input", {
195
+ class: "biab-switch",
196
+ type: "checkbox",
197
+ });
198
+ input.checked = value === true;
199
+ input.addEventListener("change", () => set(input.checked));
200
+ return el("label", { class: "biab-boolean" }, [
201
+ input,
202
+ el("span", { class: "biab-boolean__label" }, [
203
+ field.placeholder || "Yes / No",
204
+ ]),
205
+ ]);
206
+ }
207
+ case "date": {
208
+ const node = el("input", {
209
+ class: "biab-input",
210
+ type: "date",
211
+ });
212
+ node.value = str;
213
+ node.addEventListener("input", () => set(node.value));
214
+ return node;
215
+ }
216
+ case "email": {
217
+ const node = el("input", {
218
+ class: "biab-input",
219
+ type: "email",
220
+ placeholder: field.placeholder || "you@example.com",
221
+ });
222
+ setAutocomplete(node, autoComplete);
223
+ node.value = str;
224
+ node.addEventListener("input", () => set(node.value));
225
+ return node;
226
+ }
227
+ case "phone": {
228
+ const wrap = el("div", { class: "biab-phone" });
229
+ if (field.phoneConfig?.showFlags) {
230
+ wrap.append(el("span", { class: "biab-phone__flag", "aria-hidden": "true" }, [
231
+ "\u{1F1FA}\u{1F1F8}",
232
+ ]));
233
+ }
234
+ const node = el("input", {
235
+ class: "biab-input",
236
+ type: "tel",
237
+ placeholder: field.placeholder ||
238
+ (field.phoneConfig?.international
239
+ ? "+1 (555) 000-0000"
240
+ : "(555) 000-0000"),
241
+ });
242
+ setAutocomplete(node, autoComplete);
243
+ node.value = str;
244
+ node.addEventListener("input", () => set(node.value));
245
+ wrap.append(node);
246
+ return wrap;
247
+ }
248
+ case "number": {
249
+ const node = el("input", {
250
+ class: "biab-input",
251
+ type: "number",
252
+ placeholder: field.placeholder || "",
253
+ });
254
+ node.value = typeof value === "number" ? String(value) : str;
255
+ node.addEventListener("input", () => {
256
+ set(node.value === "" ? "" : Number(node.value));
257
+ });
258
+ return node;
259
+ }
260
+ case "stepper": {
261
+ const v = typeof value === "number" ? value : 0;
262
+ const out = el("span", { class: "biab-stepper__value" }, [String(v)]);
263
+ const minus = el("button", { class: "biab-btn biab-btn--ghost biab-stepper__btn", type: "button" }, ["−"]);
264
+ const plus = el("button", { class: "biab-btn biab-btn--ghost biab-stepper__btn", type: "button" }, ["+"]);
265
+ minus.addEventListener("click", () => set(v - 1));
266
+ plus.addEventListener("click", () => set(v + 1));
267
+ return el("div", { class: "biab-stepper" }, [minus, out, plus]);
268
+ }
269
+ case "slider": {
270
+ const min = field.sliderConfig?.min ?? 0;
271
+ const max = field.sliderConfig?.max ?? 100;
272
+ const step = field.sliderConfig?.step ?? 1;
273
+ const unit = field.sliderConfig?.unit;
274
+ const fmt = (n) => unit === "currency" ? `$${n}` : unit === "percent" ? `${n}%` : `${n}`;
275
+ const v = typeof value === "number" ? value : min;
276
+ const readout = el("span", { class: "biab-slider__value" }, [fmt(v)]);
277
+ const input = el("input", {
278
+ class: "biab-slider",
279
+ type: "range",
280
+ min,
281
+ max,
282
+ step,
283
+ });
284
+ input.value = String(v);
285
+ input.addEventListener("input", () => {
286
+ const n = Number(input.value);
287
+ readout.textContent = fmt(n);
288
+ set(n);
289
+ });
290
+ return el("div", { class: "biab-slider-wrap" }, [
291
+ el("div", { class: "biab-slider__scale" }, [
292
+ el("span", {}, [fmt(min)]),
293
+ readout,
294
+ el("span", {}, [fmt(max)]),
295
+ ]),
296
+ input,
297
+ ]);
298
+ }
299
+ case "star_rating": {
300
+ const max = field.starRatingConfig?.maxStars ?? 5;
301
+ const v = typeof value === "number" ? value : 0;
302
+ const wrap = el("div", { class: "biab-stars" });
303
+ for (let i = 0; i < max; i++) {
304
+ const btn = el("button", {
305
+ class: classNames("biab-star", i < v && "biab-star--filled"),
306
+ type: "button",
307
+ "aria-label": `${i + 1} star${i === 0 ? "" : "s"}`,
308
+ }, ["★"]);
309
+ btn.addEventListener("click", () => set(i + 1));
310
+ wrap.append(btn);
311
+ }
312
+ return wrap;
313
+ }
314
+ case "happiness_rating": {
315
+ const v = typeof value === "number" ? value : -1;
316
+ const wrap = el("div", { class: "biab-happiness" });
317
+ ["\u{1F61E}", "\u{1F615}", "\u{1F610}", "\u{1F642}", "\u{1F604}"].forEach((emoji, i) => {
318
+ const btn = el("button", {
319
+ class: classNames("biab-happiness__face", i === v && "biab-happiness__face--active"),
320
+ type: "button",
321
+ }, [emoji]);
322
+ btn.addEventListener("click", () => set(i));
323
+ wrap.append(btn);
324
+ });
325
+ return wrap;
326
+ }
327
+ case "color_picker": {
328
+ const v = str;
329
+ const wrap = el("div", { class: "biab-colors" });
330
+ for (const color of field.options ?? []) {
331
+ const btn = el("button", {
332
+ class: classNames("biab-color", v === color && "biab-color--selected"),
333
+ type: "button",
334
+ title: color,
335
+ style: `background-color:${color}`,
336
+ });
337
+ btn.addEventListener("click", () => set(color));
338
+ wrap.append(btn);
339
+ }
340
+ return wrap;
341
+ }
342
+ case "date_range": {
343
+ const v = value && typeof value === "object"
344
+ ? value
345
+ : { start: "", end: "" };
346
+ return twoField("Start", v.start ?? "", "End", v.end ?? "", "date", (start, end) => set({ ...v, start, end }));
347
+ }
348
+ case "time_picker": {
349
+ const mode = field.timeConfig?.mode ?? "simple";
350
+ if (mode === "simple") {
351
+ const format = field.timeConfig?.format ?? "12h";
352
+ const step = Math.max(field.timeConfig?.stepMinutes ?? 30, 1);
353
+ const node = el("select", {
354
+ class: "biab-select",
355
+ });
356
+ node.append(el("option", { value: "" }, [field.placeholder || "Select time"]));
357
+ for (let h = 0; h < 24; h++) {
358
+ for (let m = 0; m < 60; m += step) {
359
+ const t = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
360
+ const ampm = h < 12 ? "AM" : "PM";
361
+ const h12 = h % 12 === 0 ? 12 : h % 12;
362
+ const label = format === "24h"
363
+ ? t
364
+ : `${h12}:${String(m).padStart(2, "0")} ${ampm}`;
365
+ node.append(el("option", { value: t }, [label]));
366
+ }
367
+ }
368
+ node.value = str;
369
+ node.addEventListener("change", () => set(node.value));
370
+ return node;
371
+ }
372
+ if (mode === "time_block") {
373
+ const v = value && typeof value === "object"
374
+ ? value
375
+ : { start: "", end: "" };
376
+ return twoField("Start", v.start ?? "", "End", v.end ?? "", "time", (start, end) => set({ ...v, start, end }));
377
+ }
378
+ const v = value && typeof value === "object"
379
+ ? value
380
+ : { date: "", time: "" };
381
+ return twoField("Date", v.date ?? "", "Time", v.time ?? "", ["date", "time"], (date, time) => set({ ...v, date, time }));
382
+ }
383
+ case "address": {
384
+ const v = (value && typeof value === "object" ? value : {});
385
+ const grid = el("div", { class: "biab-address" });
386
+ // Fixed per-sub-field autofill tokens, suppressed to "off" when the
387
+ // composite's resolved token is "off". Mirrors the React renderer.
388
+ const part = (key, label, token, full = false) => {
389
+ const input = el("input", { class: "biab-input" });
390
+ setAutocomplete(input, acOff ? "off" : token);
391
+ input.value = v[key] ?? "";
392
+ input.addEventListener("input", () => set({ ...v, [key]: input.value }));
393
+ return el("label", {
394
+ class: classNames("biab-address__field", full && "biab-address__field--full"),
395
+ }, [el("span", { class: "biab-address__label" }, [label]), input]);
396
+ };
397
+ grid.append(part("line1", "Address Line 1", "street-address", true), part("line2", "Address Line 2", "address-line2", true), part("city", "City", "address-level2"), part("state", "State", "address-level1"), part("zip", "Zip", "postal-code", true));
398
+ return grid;
399
+ }
400
+ case "file_upload": {
401
+ const files = Array.isArray(value) ? value : [];
402
+ const maxFiles = field.fileConfig?.maxFiles ?? 1;
403
+ const allowed = field.fileConfig?.allowedTypes;
404
+ const remaining = Math.max(maxFiles - files.length, 0);
405
+ const wrap = el("div", { class: "biab-file" });
406
+ const input = el("input", {
407
+ class: "biab-file__input",
408
+ type: "file",
409
+ ...(maxFiles > 1 ? { multiple: true } : {}),
410
+ ...(allowed && allowed.length > 0 ? { accept: allowed.join(",") } : {}),
411
+ style: "display:none",
412
+ });
413
+ input.addEventListener("change", () => {
414
+ const picked = input.files;
415
+ if (picked) {
416
+ for (const f of Array.from(picked).slice(0, remaining))
417
+ upload(f);
418
+ }
419
+ input.value = "";
420
+ });
421
+ const trigger = el("button", {
422
+ class: "biab-file__trigger",
423
+ type: "button",
424
+ ...(snapshot.uploading || remaining === 0 ? { disabled: true } : {}),
425
+ }, [
426
+ snapshot.uploading
427
+ ? "Uploading…"
428
+ : remaining === 0
429
+ ? `Maximum ${maxFiles} file${maxFiles === 1 ? "" : "s"} reached`
430
+ : `Click to upload${maxFiles > 1 ? ` (up to ${remaining} more)` : ""}`,
431
+ ]);
432
+ trigger.addEventListener("click", () => input.click());
433
+ wrap.append(input, trigger);
434
+ if (files.length > 0) {
435
+ const list = el("ul", { class: "biab-file__list" });
436
+ files.forEach((f, idx) => {
437
+ const remove = el("button", {
438
+ class: "biab-file__remove",
439
+ type: "button",
440
+ "aria-label": "Remove file",
441
+ }, ["✕"]);
442
+ remove.addEventListener("click", () => set(files.filter((_, i) => i !== idx)));
443
+ list.append(el("li", { class: "biab-file__item" }, [
444
+ el("span", { class: "biab-file__name" }, [f.name]),
445
+ el("span", { class: "biab-file__size" }, [
446
+ `${Math.round(f.size / 1024)} KB`,
447
+ ]),
448
+ remove,
449
+ ]));
450
+ });
451
+ wrap.append(list);
452
+ }
453
+ return wrap;
454
+ }
455
+ default: {
456
+ const node = el("input", {
457
+ class: "biab-input",
458
+ type: "text",
459
+ placeholder: field.placeholder || "",
460
+ });
461
+ setAutocomplete(node, autoComplete);
462
+ node.value = str;
463
+ node.addEventListener("input", () => set(node.value));
464
+ return node;
465
+ }
466
+ }
467
+ }
468
+ /** Two side-by-side labelled inputs (used for date_range / time_block / appointment). */
469
+ function twoField(leftLabel, leftValue, rightLabel, rightValue, type, onChange) {
470
+ const [lt, rt] = Array.isArray(type) ? type : [type, type];
471
+ const left = el("input", {
472
+ class: "biab-input",
473
+ type: lt,
474
+ });
475
+ const right = el("input", {
476
+ class: "biab-input",
477
+ type: rt,
478
+ });
479
+ left.value = leftValue;
480
+ right.value = rightValue;
481
+ left.addEventListener("input", () => onChange(left.value, right.value));
482
+ right.addEventListener("input", () => onChange(left.value, right.value));
483
+ return el("div", { class: "biab-grid biab-grid--2" }, [
484
+ el("label", { class: "biab-grid__cell" }, [
485
+ el("span", { class: "biab-grid__label" }, [leftLabel]),
486
+ left,
487
+ ]),
488
+ el("label", { class: "biab-grid__cell" }, [
489
+ el("span", { class: "biab-grid__label" }, [rightLabel]),
490
+ right,
491
+ ]),
492
+ ]);
493
+ }
494
+ function renderFieldRow(field, ctx, flat) {
495
+ const errored = Boolean(ctx.snapshot.errors[field.id]);
496
+ const showLabel = !NO_LABEL_TYPES.has(field.type);
497
+ const children = [];
498
+ if (showLabel && field.label) {
499
+ children.push(el("label", { class: "biab-field__label" }, [
500
+ field.label,
501
+ field.required
502
+ ? el("span", { class: "biab-field__required" }, [" *"])
503
+ : null,
504
+ ]));
505
+ }
506
+ const input = renderFieldInput(field, ctx.snapshot, (v) => ctx.setValue(field.id, v), (file) => ctx.upload(field.id, file), resolveFieldAutocomplete(field, ctx.autocompleteConfig));
507
+ if (input)
508
+ children.push(input);
509
+ if (errored) {
510
+ children.push(el("p", { class: "biab-field__error" }, [
511
+ ctx.snapshot.errors[field.id]?.message ?? "This field is required.",
512
+ ]));
513
+ }
514
+ if (field.helpText) {
515
+ children.push(el("p", { class: "biab-field__help" }, [field.helpText]));
516
+ }
517
+ // `flat` is accepted for signature parity with the group renderer; rows look
518
+ // the same flat or nested.
519
+ void flat;
520
+ return el("div", {
521
+ class: classNames("biab-field", `biab-field--${field.type}`, field.required && "biab-field--required", errored && "biab-field--error"),
522
+ "data-field-id": field.id,
523
+ "data-biab-field": fieldCssKey(field),
524
+ "data-biab-type": field.type,
525
+ }, children);
526
+ }
527
+ /** Recursively render a list of fields, handling every layout/condition node. */
528
+ function renderTree(fields, ctx, flat) {
529
+ const container = el("div", { class: "biab-tree" });
530
+ for (const field of fields) {
531
+ if (field.type === "input_group") {
532
+ const groupChildren = (field.children ?? []);
533
+ const concurrent = field.concurrentReveal === true;
534
+ const revealCount = concurrent
535
+ ? concurrentRevealCount(groupChildren, ctx.snapshot.values)
536
+ : groupChildren.length;
537
+ const visible = concurrent
538
+ ? groupChildren.slice(0, revealCount)
539
+ : groupChildren;
540
+ const groupNode = el("div", {
541
+ class: classNames("biab-group", flat ? "biab-group--flat" : "biab-group--card"),
542
+ "data-biab-group": fieldCssKey(field),
543
+ }, []);
544
+ if (field.label && field.showGroupLabel === true) {
545
+ groupNode.append(el("div", { class: "biab-group__label" }, [field.label]));
546
+ }
547
+ groupNode.append(renderTree(visible, ctx, flat));
548
+ container.append(groupNode);
549
+ continue;
550
+ }
551
+ if (field.type === "condition_block") {
552
+ if (!evalCondition(field.conditionRule, ctx.snapshot.values))
553
+ continue;
554
+ container.append(renderTree((field.children ?? []), ctx, flat));
555
+ continue;
556
+ }
557
+ if (field.type === "or_condition") {
558
+ const errored = Boolean(ctx.snapshot.errors[field.id]);
559
+ const left = field.children?.[0];
560
+ const right = field.children?.[1];
561
+ const box = el("div", { class: classNames("biab-or", errored && "biab-or--error") }, []);
562
+ if (field.label) {
563
+ box.append(el("div", { class: "biab-or__label" }, [
564
+ field.label,
565
+ el("span", { class: "biab-or__hint" }, [" Fill at least one side"]),
566
+ ]));
567
+ }
568
+ const grid = el("div", { class: "biab-or__grid" }, [
569
+ el("div", { class: "biab-or__side" }, [
570
+ el("div", { class: "biab-or__side-label" }, [
571
+ field.orConditionConfig?.leftLabel || "Option A",
572
+ ]),
573
+ left ? renderTree([left], ctx, flat) : null,
574
+ ]),
575
+ el("div", { class: "biab-or__divider" }, [
576
+ el("span", { class: "biab-or__badge" }, ["OR"]),
577
+ ]),
578
+ el("div", { class: "biab-or__side" }, [
579
+ el("div", { class: "biab-or__side-label" }, [
580
+ field.orConditionConfig?.rightLabel || "Option B",
581
+ ]),
582
+ right ? renderTree([right], ctx, flat) : null,
583
+ ]),
584
+ ]);
585
+ box.append(grid);
586
+ if (errored) {
587
+ box.append(el("p", { class: "biab-or__error" }, [
588
+ "Fill in at least one side to continue.",
589
+ ]));
590
+ }
591
+ container.append(box);
592
+ continue;
593
+ }
594
+ if (field.type === "flex_row") {
595
+ const row = el("div", { class: "biab-flex-row" }, []);
596
+ for (const child of (field.children ?? [])) {
597
+ row.append(el("div", { class: "biab-flex-row__cell" }, [
598
+ renderFieldRow(child, ctx, flat),
599
+ ]));
600
+ }
601
+ container.append(row);
602
+ continue;
603
+ }
604
+ container.append(renderFieldRow(field, ctx, flat));
605
+ }
606
+ return container;
607
+ }
608
+ // ─────────────────────────────────────────────────────────────────────────────
609
+ // Progress indicators — port of FormPreview's steps / bar variants.
610
+ // ─────────────────────────────────────────────────────────────────────────────
611
+ function renderProgress(snapshot, onJump) {
612
+ const { settings, steps, step, stepCount } = snapshot;
613
+ if (!settings.isConsecutive || !settings.showProgress || stepCount <= 1)
614
+ return null;
615
+ if (settings.progressStyle === "bar") {
616
+ const fill = el("div", { class: "biab-progress__fill" });
617
+ fill.style.width = `${((step + 1) / stepCount) * 100}%`;
618
+ return el("div", { class: "biab-progress biab-progress--bar" }, [
619
+ el("span", { class: "biab-progress__count" }, [
620
+ `${step + 1} / ${stepCount}`,
621
+ ]),
622
+ el("div", { class: "biab-progress__track" }, [fill]),
623
+ ]);
624
+ }
625
+ const track = el("div", { class: "biab-progress__steps" });
626
+ steps.forEach((s, i) => {
627
+ const f = s[0];
628
+ const title = (typeof f?.stepTitle === "string" && f.stepTitle) ||
629
+ f?.label ||
630
+ `Step ${i + 1}`;
631
+ const done = i < step;
632
+ const current = i === step;
633
+ if (i > 0) {
634
+ track.append(el("div", {
635
+ class: classNames("biab-progress__line", i <= step && "biab-progress__line--done"),
636
+ }));
637
+ }
638
+ const dot = el("span", {
639
+ class: classNames("biab-progress__dot", current && "biab-progress__dot--current", done && "biab-progress__dot--done"),
640
+ }, [done ? "✓" : String(i + 1)]);
641
+ const stepNode = el("button", {
642
+ class: classNames("biab-progress__step", current && "biab-progress__step--current"),
643
+ type: "button",
644
+ }, [dot, el("span", { class: "biab-progress__title" }, [title])]);
645
+ stepNode.addEventListener("click", () => onJump(i));
646
+ track.append(stepNode);
647
+ });
648
+ return el("div", { class: "biab-progress biab-progress--steps" }, [
649
+ el("div", { class: "biab-progress__heading" }, ["Form Progress"]),
650
+ track,
651
+ ]);
652
+ }
653
+ // ─────────────────────────────────────────────────────────────────────────────
654
+ // mountBiabForm — wire a controller to a host element and render reactively.
655
+ // ─────────────────────────────────────────────────────────────────────────────
656
+ function resolveHost(elOrSelector) {
657
+ const host = typeof elOrSelector === "string"
658
+ ? document.querySelector(elOrSelector)
659
+ : elOrSelector;
660
+ if (!host) {
661
+ throw new Error(`mountBiabForm: no element found for ${typeof elOrSelector === "string" ? `selector "${elOrSelector}"` : "the provided node"}.`);
662
+ }
663
+ return host;
664
+ }
665
+ export function mountBiabForm(elOrSelector, opts) {
666
+ const host = resolveHost(elOrSelector);
667
+ const extraClass = opts.class ?? opts.className;
668
+ // HTML autofill config built from the mount options, threaded to every field.
669
+ const autocompleteConfig = {
670
+ ...(opts.autoComplete !== undefined ? { enabled: opts.autoComplete } : {}),
671
+ ...(opts.fieldAutoComplete ? { overrides: opts.fieldAutoComplete } : {}),
672
+ };
673
+ const controllerOptions = {
674
+ ...(opts.client ? { client: opts.client } : {}),
675
+ ...(opts.slug ? { slug: opts.slug } : {}),
676
+ ...(opts.schema ? { schema: opts.schema } : {}),
677
+ ...(opts.submitOptions ? { submitOptions: opts.submitOptions } : {}),
678
+ ...(opts.onSuccess ? { onSuccess: opts.onSuccess } : {}),
679
+ ...(opts.onError ? { onError: opts.onError } : {}),
680
+ };
681
+ const controller = createFormController(controllerOptions);
682
+ const doSubmit = (callOpts) => {
683
+ opts.onSubmit?.(controller.snapshot().values);
684
+ const merged = {
685
+ ...(opts.dryRun ? { dryRun: true } : {}),
686
+ ...callOpts,
687
+ };
688
+ return controller.submit(merged);
689
+ };
690
+ /** Swap the host's content, preserving focus + caret across the rebuild.
691
+ * Every notify re-renders the whole tree (conditions / reveal change which
692
+ * fields exist), which would otherwise steal focus mid-typing. We re-locate
693
+ * the focused input by its owning field row + ordinal and restore the caret. */
694
+ function swap(root) {
695
+ const active = document.activeElement;
696
+ let restore = null;
697
+ if (active &&
698
+ host.contains(active) &&
699
+ (active instanceof HTMLInputElement ||
700
+ active instanceof HTMLTextAreaElement ||
701
+ active instanceof HTMLSelectElement)) {
702
+ const row = active.closest("[data-field-id]");
703
+ const fieldId = row?.getAttribute("data-field-id");
704
+ if (fieldId && row) {
705
+ const inputs = Array.from(row.querySelectorAll("input, textarea, select"));
706
+ const index = inputs.indexOf(active);
707
+ const canCaret = active instanceof HTMLInputElement ||
708
+ active instanceof HTMLTextAreaElement;
709
+ restore = {
710
+ fieldId,
711
+ index,
712
+ start: canCaret ? active.selectionStart : null,
713
+ end: canCaret ? active.selectionEnd : null,
714
+ };
715
+ }
716
+ }
717
+ host.replaceChildren(root);
718
+ if (restore) {
719
+ const row = host.querySelector(`[data-field-id="${cssEscape(restore.fieldId)}"]`);
720
+ const inputs = row
721
+ ? Array.from(row.querySelectorAll("input, textarea, select"))
722
+ : [];
723
+ const next = inputs[restore.index];
724
+ if (next) {
725
+ next.focus();
726
+ if (restore.start != null &&
727
+ (next instanceof HTMLInputElement ||
728
+ next instanceof HTMLTextAreaElement)) {
729
+ try {
730
+ next.setSelectionRange(restore.start, restore.end ?? restore.start);
731
+ }
732
+ catch {
733
+ // `setSelectionRange` throws on input types that don't support it
734
+ // (e.g. number / email / date) — focus alone is enough there.
735
+ }
736
+ }
737
+ }
738
+ }
739
+ }
740
+ function render(snapshot) {
741
+ const root = el("div", {
742
+ class: classNames("biab-form", extraClass),
743
+ "data-biab-status": snapshot.status,
744
+ });
745
+ // Org icon (when the form opted into showOrgIcon).
746
+ if (snapshot.orgIcon) {
747
+ root.append(el("img", {
748
+ class: "biab-form__org-icon",
749
+ src: snapshot.orgIcon,
750
+ alt: "",
751
+ }));
752
+ }
753
+ // Loading / error states.
754
+ if (snapshot.status === "loading" || snapshot.status === "idle") {
755
+ root.append(el("div", { class: "biab-form__loading" }, ["Loading…"]));
756
+ swap(root);
757
+ return;
758
+ }
759
+ if (snapshot.status === "error") {
760
+ root.append(el("div", { class: "biab-form__error" }, [
761
+ snapshot.loadError || "Could not load this form.",
762
+ ]));
763
+ swap(root);
764
+ return;
765
+ }
766
+ // Submitted confirmation.
767
+ if (snapshot.submitted) {
768
+ root.append(el("div", { class: "biab-form__success" }, [
769
+ el("p", { class: "biab-form__success-title" }, [
770
+ "Thanks — we got it.",
771
+ ]),
772
+ ]));
773
+ swap(root);
774
+ return;
775
+ }
776
+ // Header (title / description).
777
+ if (snapshot.title || snapshot.description) {
778
+ const header = el("div", { class: "biab-form__header" }, []);
779
+ if (snapshot.title)
780
+ header.append(el("h2", { class: "biab-form__title" }, [snapshot.title]));
781
+ if (snapshot.description) {
782
+ header.append(el("p", { class: "biab-form__description" }, [snapshot.description]));
783
+ }
784
+ root.append(header);
785
+ }
786
+ // Progress indicator.
787
+ const progress = renderProgress(snapshot, (i) => controller.goToStep(i));
788
+ if (progress)
789
+ root.append(progress);
790
+ // Error summary banner.
791
+ if (snapshot.errorList.length > 0) {
792
+ root.append(el("div", { class: "biab-form__error-summary" }, [
793
+ el("div", { class: "biab-form__error-summary-title" }, [
794
+ `Please complete the highlighted required field${snapshot.errorList.length === 1 ? "" : "s"}.`,
795
+ ]),
796
+ el("div", { class: "biab-form__error-summary-list" }, [
797
+ snapshot.errorList.map((e) => e.label).join(", "),
798
+ ]),
799
+ ]));
800
+ }
801
+ const ctx = {
802
+ snapshot,
803
+ setValue: (id, v) => controller.setValue(id, v),
804
+ upload: (fieldId, file) => {
805
+ void controller.uploadFile(fieldId, file);
806
+ },
807
+ autocompleteConfig,
808
+ };
809
+ // Field tree (animation-wrapped active step).
810
+ const flat = snapshot.settings.isConsecutive;
811
+ const stepWrap = el("div", {
812
+ class: animClass(snapshot.settings.animation, snapshot.forward),
813
+ "data-biab-step": String(snapshot.step),
814
+ });
815
+ if (snapshot.settings.animation === "scroll") {
816
+ // Reveal each top-level field on scroll. Class is the hook; CSS owns the
817
+ // transition. We add the visible state once it intersects.
818
+ for (const vf of snapshot.visibleFields) {
819
+ const reveal = el("div", { class: "biab-reveal" }, [
820
+ renderTree([vf], ctx, flat),
821
+ ]);
822
+ stepWrap.append(reveal);
823
+ observeReveal(reveal);
824
+ }
825
+ }
826
+ else {
827
+ stepWrap.append(renderTree(snapshot.visibleFields, ctx, flat));
828
+ }
829
+ root.append(stepWrap);
830
+ // Navigation / submit.
831
+ root.append(renderNav(snapshot));
832
+ swap(root);
833
+ }
834
+ function renderNav(snapshot) {
835
+ const submitLabel = opts.submitLabel ?? "Submit";
836
+ if (snapshot.settings.isConsecutive && snapshot.stepCount > 1) {
837
+ const back = el("button", {
838
+ class: "biab-btn biab-btn--ghost biab-btn--back",
839
+ type: "button",
840
+ ...(snapshot.isFirstStep ? { disabled: true } : {}),
841
+ }, ["Back"]);
842
+ back.addEventListener("click", () => controller.back());
843
+ let advance;
844
+ if (!snapshot.isLastStep) {
845
+ advance = el("button", {
846
+ class: "biab-btn biab-btn--next",
847
+ type: "button",
848
+ ...(snapshot.canAdvance ? {} : { disabled: true }),
849
+ }, ["Next"]);
850
+ advance.addEventListener("click", () => controller.next());
851
+ }
852
+ else {
853
+ advance = el("button", {
854
+ class: "biab-btn biab-btn--submit",
855
+ type: "button",
856
+ ...(snapshot.submitting ? { disabled: true } : {}),
857
+ }, [snapshot.submitting ? "Sending…" : submitLabel]);
858
+ advance.addEventListener("click", () => void doSubmit());
859
+ }
860
+ return el("div", { class: "biab-nav" }, [back, advance]);
861
+ }
862
+ const submit = el("button", {
863
+ class: "biab-btn biab-btn--submit",
864
+ type: "button",
865
+ ...(snapshot.submitting ? { disabled: true } : {}),
866
+ }, [snapshot.submitting ? "Sending…" : submitLabel]);
867
+ submit.addEventListener("click", () => void doSubmit());
868
+ return el("div", { class: "biab-nav" }, [submit]);
869
+ }
870
+ const unsubscribe = controller.subscribe(render);
871
+ // Initial paint + load (load() notifies, which re-renders).
872
+ render(controller.snapshot());
873
+ void controller.load();
874
+ return {
875
+ controller,
876
+ reload: () => controller.load(),
877
+ submit: doSubmit,
878
+ reset: () => controller.reset(),
879
+ destroy: () => {
880
+ unsubscribe();
881
+ controller.destroy();
882
+ host.replaceChildren();
883
+ },
884
+ };
885
+ }
886
+ /** Add the `biab-reveal--shown` hook once an element scrolls into view (scroll anim). */
887
+ function observeReveal(node) {
888
+ if (typeof IntersectionObserver === "undefined") {
889
+ node.classList.add("biab-reveal--shown");
890
+ return;
891
+ }
892
+ const obs = new IntersectionObserver((entries) => {
893
+ for (const entry of entries) {
894
+ if (entry.isIntersecting) {
895
+ node.classList.add("biab-reveal--shown");
896
+ obs.disconnect();
897
+ break;
898
+ }
899
+ }
900
+ }, { threshold: 0.15, rootMargin: "0px 0px -8% 0px" });
901
+ obs.observe(node);
902
+ }
903
+ //# sourceMappingURL=vanilla.js.map