@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
package/src/qwik.tsx ADDED
@@ -0,0 +1,1234 @@
1
+ /** @jsxImportSource @builder.io/qwik */
2
+ /**
3
+ * `@businessdash/sdk/qwik` — the `<BiabForm>` drop-in for Qwik / Qwik City.
4
+ *
5
+ * Ship mode: SOURCE (.tsx). The consumer's Qwik optimizer compiles this file —
6
+ * it is never tsc-built into `dist`. Keep it syntactically valid Qwik JSX and
7
+ * self-consistent against the headless core's PUBLIC contract; do not reach
8
+ * into anything but `@businessdash/sdk/forms`.
9
+ *
10
+ * ─── Architecture ────────────────────────────────────────────────────────────
11
+ * The headless `createFormController` (`@businessdash/sdk/forms`) owns ALL of the
12
+ * form's runtime state and behaviour — values, condition-aware visibility,
13
+ * validation mirrored 1:1 from the BIAB server, multi-step nav + progress,
14
+ * concurrent-reveal gating, the submit lifecycle, and file uploads. This binding
15
+ * stays dumb: it
16
+ * 1. builds the controller (client-side, inside `useVisibleTask$`),
17
+ * 2. mirrors its `snapshot()` into a Qwik `useStore` on every `subscribe`
18
+ * notify (the idiomatic bridge from an external observable into Qwik
19
+ * reactivity), and
20
+ * 3. renders the nested field tree by `type` using the `biab-*` light-DOM
21
+ * class convention — the SAME classes the dashboard `FormPreview` renderer
22
+ * uses, so a consumer's `biab-*` CSS overrides cascade identically and the
23
+ * drop-in reproduces the dashboard preview (animation, multi-step progress
24
+ * bar/labels, concurrent reveal, org icon).
25
+ *
26
+ * ─── Why a client-side controller + a `submit$` QRL ──────────────────────────
27
+ * Qwik can't serialize the controller (a closure-over-state observable) across
28
+ * the resumability boundary, so it is created on the client from a SERIALIZABLE
29
+ * pre-fetched `schema` (the SSR pattern the Qwik starter already uses with
30
+ * `routeLoader$`). The bearer key must never enter the client bundle, so the
31
+ * actual `forms.submit` round-trip is delegated to a `submit$` QRL the consumer
32
+ * supplies (a `server$` RPC). When a `client` is passed directly (e.g. a public
33
+ * key in a fully client-rendered app) the controller's own `client.forms.submit`
34
+ * is used and `submit$` is optional.
35
+ */
36
+
37
+ import {
38
+ component$,
39
+ type NoSerialize,
40
+ noSerialize,
41
+ type QRL,
42
+ Slot,
43
+ useSignal,
44
+ useStore,
45
+ useVisibleTask$,
46
+ $,
47
+ } from "@builder.io/qwik";
48
+
49
+ import type { BiabClient } from "@businessdash/sdk";
50
+ import {
51
+ type FormCoreField,
52
+ type FormController,
53
+ type FormSchema,
54
+ type FormSnapshot,
55
+ type FormSubmitOptions,
56
+ type FormSubmitResult,
57
+ concurrentRevealCount,
58
+ createFormController,
59
+ evalCondition,
60
+ toSubmissionPayload,
61
+ } from "@businessdash/sdk/forms";
62
+ import {
63
+ type AutocompleteConfig,
64
+ resolveFieldAutocomplete,
65
+ } from "./forms-core/autocomplete.js";
66
+
67
+ /**
68
+ * A QRL the binding can `await` and call with `(...args)`, accepting BOTH a
69
+ * plain client `$()` QRL AND a `server$()`-produced `ServerQRL`. `server$`
70
+ * widens its inner function to `((abort: AbortSignal, ...args) => R) | ((...args)
71
+ * => R)`, so we mirror that union here — letting consumers pass a `server$` RPC
72
+ * directly without importing `@builder.io/qwik-city` types into this binding.
73
+ * The leading `AbortSignal` is injected by the Qwik runtime, never by us.
74
+ */
75
+ type ServerCompatQRL<Args extends unknown[], R> = QRL<
76
+ | ((...args: Args) => Promise<R>)
77
+ | ((abort: AbortSignal, ...args: Args) => Promise<R>)
78
+ // server$ handlers commonly declare ONLY the first argument (the payload).
79
+ // Plain function variance would allow that, but QRL's type parameter is
80
+ // invariant, so the narrower shapes must be named explicitly — without
81
+ // these, `server$((payload) => …)` from qwik-city ^1.20 fails to assign.
82
+ | ((first: Args[0]) => Promise<R>)
83
+ | ((abort: AbortSignal, first: Args[0]) => Promise<R>)
84
+ >;
85
+
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+ // Public props — consistent with the other framework bindings.
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+
90
+ export interface BiabFormProps {
91
+ /**
92
+ * Pre-fetched form schema (SSR / `routeLoader$`). PREFERRED on Qwik — it is
93
+ * serializable, keeps the bearer key server-side, and renders without a
94
+ * client-side flicker. Either `schema` or `client` (+`slug`) is required.
95
+ */
96
+ schema?: FormSchema;
97
+ /**
98
+ * SDK client. Only safe in a fully client-rendered app with a public key —
99
+ * it pulls the schema and (unless `submit$` is given) submits from the
100
+ * browser. For SSR apps, prefer `schema` + `submit$`.
101
+ */
102
+ client?: BiabClient;
103
+ /** Form slug. Required with `client` (the pre-fetched `schema` carries its own). */
104
+ slug?: string;
105
+
106
+ /**
107
+ * Server-side submit delegate (a `server$` QRL). Receives the re-keyed
108
+ * submission payload (output keys) + the merged submit options, and must
109
+ * return the SDK's `FormSubmitResult`. This is how the bearer key stays out
110
+ * of the client bundle. Optional when a `client` is supplied.
111
+ */
112
+ submit$?: ServerCompatQRL<
113
+ [payload: Record<string, unknown>, options: FormSubmitOptions],
114
+ FormSubmitResult
115
+ >;
116
+ /**
117
+ * Server-side file-upload delegate (a `server$` QRL) for `file_upload`
118
+ * fields. Receives the field id + the picked File and must return the SDK's
119
+ * `FormFileValue` (`{ storageId, url, name, mimeType, size }`). Required for
120
+ * uploads in the SSR (no-client) path. Optional when a `client` is supplied.
121
+ */
122
+ uploadFile$?: ServerCompatQRL<
123
+ [fieldId: string, file: File],
124
+ {
125
+ storageId: string;
126
+ url: string;
127
+ name: string;
128
+ mimeType: string;
129
+ size: number;
130
+ }
131
+ >;
132
+
133
+ /** Provenance / recipient passthrough merged into every submit (call wins). */
134
+ submitOptions?: FormSubmitOptions;
135
+ /** Convenience: lead-source provenance, folded into `submitOptions.source`. */
136
+ source?: string;
137
+ /** Test mode — validate server-side, store nothing, no side effects. */
138
+ dryRun?: boolean;
139
+
140
+ /** Label for the final submit button. Default "Submit". */
141
+ submitLabel?: string;
142
+
143
+ /**
144
+ * HTML autofill global switch. Default infers a sensible `autocomplete` token
145
+ * per field; `false` emits `autocomplete="off"` on every value-bearing input.
146
+ */
147
+ autoComplete?: boolean;
148
+ /**
149
+ * Per-field autofill overrides keyed by field id: a literal token, `true`
150
+ * (infer), or `false` (off). Wins over `autoComplete` and any schema token.
151
+ */
152
+ fieldAutoComplete?: Record<string, string | boolean>;
153
+
154
+ /** Fires with the WHOLE re-keyed payload when local validation passes, before
155
+ * the network round-trip. Read-only hook for analytics / optimistic UI. */
156
+ onSubmit$?: QRL<(payload: Record<string, unknown>) => void>;
157
+ /** Fires after a successful submit (`result.ok === true`). */
158
+ onSuccess$?: QRL<(result: Extract<FormSubmitResult, { ok: true }>) => void>;
159
+ /** Fires after a failed submit (`result.ok === false`). */
160
+ onError$?: QRL<(result: Extract<FormSubmitResult, { ok: false }>) => void>;
161
+
162
+ /** Extra class on the root `.biab-form`. Both `class` and `className` work. */
163
+ class?: string;
164
+ className?: string;
165
+ }
166
+
167
+ // ─────────────────────────────────────────────────────────────────────────────
168
+ // A serializable mirror of the snapshot. Qwik's `useStore` deep-tracks plain
169
+ // data; the controller's `FormSnapshot` is plain data + nested field trees, so
170
+ // we copy the whole thing in. `result` / `schema` are plain JSON too.
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+
173
+ type SnapshotState = { snap: FormSnapshot | null };
174
+
175
+ /** Read-shape of the signal holding the (client-only) controller. */
176
+ type ControllerRef = { value: FormController | undefined };
177
+
178
+ /** The resolved server-side upload delegate threaded to `file_upload` fields. */
179
+ type UploadDelegate = BiabFormProps["uploadFile$"];
180
+
181
+ /**
182
+ * Build the initial snapshot for SSR from a pre-fetched schema so the fields
183
+ * render immediately (no client flicker). `createFormController({ schema })` is
184
+ * a pure synchronous factory — no client / network — and `snapshot()` returns
185
+ * plain serializable data, which is exactly what Qwik needs to resume.
186
+ */
187
+ function seedSnapshot(schema: FormSchema | undefined): FormSnapshot | null {
188
+ if (!schema) return null;
189
+ const ctrl = createFormController({ schema });
190
+ // `load()` adopts the pre-fetched schema synchronously (no await needed for
191
+ // the snapshot to become `ready`), but the synchronous factory already seeds
192
+ // a valid pre-load snapshot; calling load here keeps status === "ready".
193
+ void ctrl.load();
194
+ const snap = ctrl.snapshot();
195
+ ctrl.destroy();
196
+ return snap;
197
+ }
198
+
199
+ // ─────────────────────────────────────────────────────────────────────────────
200
+ // Field rendering — a faithful port of the dashboard `FormPreview` per-type
201
+ // switch, using `biab-*` classes (light DOM). Qwik JSX uses `class`, not
202
+ // `className`, and `onInput$` / `onClick$` event QRLs.
203
+ // ─────────────────────────────────────────────────────────────────────────────
204
+
205
+ /** Static / layout types that never carry a submitted value (skip the label). */
206
+ const NO_LABEL_TYPES = new Set([
207
+ "title",
208
+ "subtitle",
209
+ "separator",
210
+ "richtext",
211
+ "image",
212
+ ]);
213
+
214
+ /** Stable, override-friendly CSS hook for a field (the `data-biab-field` value). */
215
+ function fieldCssKey(field: FormCoreField): string {
216
+ const explicit =
217
+ typeof field.outputKey === "string" ? field.outputKey.trim() : "";
218
+ if (explicit) return explicit;
219
+ if (typeof field.key === "string" && field.key) return field.key;
220
+ return field.id;
221
+ }
222
+
223
+ function classOf(...parts: Array<string | false | null | undefined>): string {
224
+ return parts.filter(Boolean).join(" ");
225
+ }
226
+
227
+ function asString(value: unknown): string {
228
+ return typeof value === "string" ? value : "";
229
+ }
230
+
231
+ /**
232
+ * The leaf input for one value-bearing field. Reads/writes through `state.snap`
233
+ * via the controller's `setValue` / `uploadFile`. Renders by `type` exactly as
234
+ * the dashboard preview does.
235
+ */
236
+ const BiabFieldInput = component$<{
237
+ field: FormCoreField;
238
+ controllerSig: ControllerRef;
239
+ state: SnapshotState;
240
+ uploadFn?: UploadDelegate;
241
+ acConfig: AutocompleteConfig;
242
+ }>(({ field, controllerSig, state, uploadFn, acConfig }) => {
243
+ const value = state.snap?.values?.[field.id];
244
+
245
+ const set = $((v: unknown) => {
246
+ controllerSig.value?.setValue(field.id, v);
247
+ });
248
+
249
+ // Resolved HTML autofill token for this field (or `undefined` to omit the
250
+ // attribute). Honors the global `autoComplete` switch + per-field overrides;
251
+ // see forms-core/autocomplete.ts.
252
+ const autoComplete = resolveFieldAutocomplete(field, acConfig);
253
+
254
+ switch (field.type) {
255
+ case "title":
256
+ return <h2 class="biab-field__title">{field.label || "Untitled"}</h2>;
257
+ case "subtitle":
258
+ return <h3 class="biab-field__subtitle">{field.label || ""}</h3>;
259
+ case "separator":
260
+ return <hr class="biab-field__separator" />;
261
+ case "richtext":
262
+ // `content` is server-sanitized HTML on publish.
263
+ return field.content ? (
264
+ <div class="biab-field__richtext" dangerouslySetInnerHTML={field.content} />
265
+ ) : null;
266
+ case "image":
267
+ return field.content ? (
268
+ <img class="biab-field__image" src={field.content} alt={field.label || ""} />
269
+ ) : null;
270
+
271
+ case "textarea":
272
+ return (
273
+ <textarea
274
+ class="biab-input biab-textarea"
275
+ value={asString(value)}
276
+ placeholder={field.placeholder || ""}
277
+ rows={4}
278
+ autocomplete={autoComplete as AutoFill | undefined}
279
+ onInput$={(_, el) => void set((el as HTMLTextAreaElement).value)}
280
+ />
281
+ );
282
+
283
+ case "select":
284
+ return (
285
+ <select
286
+ class="biab-input biab-select"
287
+ value={asString(value)}
288
+ onChange$={(_, el) => void set((el as HTMLSelectElement).value)}
289
+ >
290
+ <option value="">{field.placeholder || "Select..."}</option>
291
+ {(field.options ?? []).map((opt) => (
292
+ <option key={opt} value={opt}>
293
+ {opt}
294
+ </option>
295
+ ))}
296
+ </select>
297
+ );
298
+
299
+ case "radio":
300
+ return (
301
+ <div class="biab-radio-group">
302
+ {(field.options ?? []).map((opt) => (
303
+ <label class="biab-radio" key={opt}>
304
+ <input
305
+ type="radio"
306
+ name={field.id}
307
+ value={opt}
308
+ checked={asString(value) === opt}
309
+ onChange$={() => void set(opt)}
310
+ />
311
+ <span>{opt}</span>
312
+ </label>
313
+ ))}
314
+ </div>
315
+ );
316
+
317
+ case "checkbox": {
318
+ const arr = Array.isArray(value) ? (value as string[]) : [];
319
+ return (
320
+ <div class="biab-checkbox-group">
321
+ {(field.options ?? []).map((opt) => {
322
+ const checked = arr.includes(opt);
323
+ return (
324
+ <label class="biab-checkbox" key={opt}>
325
+ <input
326
+ type="checkbox"
327
+ checked={checked}
328
+ onChange$={(_, el) => {
329
+ const next = (el as HTMLInputElement).checked
330
+ ? [...arr, opt]
331
+ : arr.filter((x) => x !== opt);
332
+ void set(next);
333
+ }}
334
+ />
335
+ <span>{opt}</span>
336
+ </label>
337
+ );
338
+ })}
339
+ </div>
340
+ );
341
+ }
342
+
343
+ case "boolean":
344
+ return (
345
+ <label class="biab-switch">
346
+ <input
347
+ type="checkbox"
348
+ checked={value === true}
349
+ onChange$={(_, el) => void set((el as HTMLInputElement).checked)}
350
+ />
351
+ <span class="biab-switch__label">{field.placeholder || "Yes / No"}</span>
352
+ </label>
353
+ );
354
+
355
+ case "date":
356
+ return (
357
+ <input
358
+ class="biab-input biab-date"
359
+ type="date"
360
+ value={asString(value)}
361
+ onInput$={(_, el) => void set((el as HTMLInputElement).value)}
362
+ />
363
+ );
364
+
365
+ case "date_range": {
366
+ const v =
367
+ value && typeof value === "object"
368
+ ? (value as { start?: string; end?: string })
369
+ : { start: "", end: "" };
370
+ return (
371
+ <div class="biab-date-range">
372
+ <label class="biab-date-range__field">
373
+ <span class="biab-date-range__label">Start</span>
374
+ <input
375
+ class="biab-input"
376
+ type="date"
377
+ value={v.start ?? ""}
378
+ onInput$={(_, el) =>
379
+ void set({ ...v, start: (el as HTMLInputElement).value })
380
+ }
381
+ />
382
+ </label>
383
+ <label class="biab-date-range__field">
384
+ <span class="biab-date-range__label">End</span>
385
+ <input
386
+ class="biab-input"
387
+ type="date"
388
+ value={v.end ?? ""}
389
+ onInput$={(_, el) =>
390
+ void set({ ...v, end: (el as HTMLInputElement).value })
391
+ }
392
+ />
393
+ </label>
394
+ </div>
395
+ );
396
+ }
397
+
398
+ case "time_picker": {
399
+ const mode = field.timeConfig?.mode ?? "simple";
400
+ if (mode === "simple") {
401
+ const step = Math.max(field.timeConfig?.stepMinutes ?? 30, 1);
402
+ const format = field.timeConfig?.format ?? "12h";
403
+ const opts: string[] = [];
404
+ for (let h = 0; h < 24; h++) {
405
+ for (let m = 0; m < 60; m += step) {
406
+ opts.push(
407
+ `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`,
408
+ );
409
+ }
410
+ }
411
+ const fmt = (t: string) => {
412
+ if (format === "24h") return t;
413
+ const [hStr, mStr = "00"] = t.split(":");
414
+ const h = Number(hStr);
415
+ const ampm = h < 12 ? "AM" : "PM";
416
+ const h12 = h % 12 === 0 ? 12 : h % 12;
417
+ return `${h12}:${mStr} ${ampm}`;
418
+ };
419
+ return (
420
+ <select
421
+ class="biab-input biab-select biab-time"
422
+ value={asString(value)}
423
+ onChange$={(_, el) => void set((el as HTMLSelectElement).value)}
424
+ >
425
+ <option value="">{field.placeholder || "Select time"}</option>
426
+ {opts.map((o) => (
427
+ <option key={o} value={o}>
428
+ {fmt(o)}
429
+ </option>
430
+ ))}
431
+ </select>
432
+ );
433
+ }
434
+ if (mode === "time_block") {
435
+ const v =
436
+ value && typeof value === "object"
437
+ ? (value as { start?: string; end?: string })
438
+ : { start: "", end: "" };
439
+ return (
440
+ <div class="biab-time-block">
441
+ <label class="biab-time-block__field">
442
+ <span class="biab-time-block__label">Start</span>
443
+ <input
444
+ class="biab-input"
445
+ type="time"
446
+ value={v.start ?? ""}
447
+ onInput$={(_, el) =>
448
+ void set({ ...v, start: (el as HTMLInputElement).value })
449
+ }
450
+ />
451
+ </label>
452
+ <label class="biab-time-block__field">
453
+ <span class="biab-time-block__label">End</span>
454
+ <input
455
+ class="biab-input"
456
+ type="time"
457
+ value={v.end ?? ""}
458
+ onInput$={(_, el) =>
459
+ void set({ ...v, end: (el as HTMLInputElement).value })
460
+ }
461
+ />
462
+ </label>
463
+ </div>
464
+ );
465
+ }
466
+ // appointment
467
+ const v =
468
+ value && typeof value === "object"
469
+ ? (value as { date?: string; time?: string })
470
+ : { date: "", time: "" };
471
+ return (
472
+ <div class="biab-appointment">
473
+ <label class="biab-appointment__field">
474
+ <span class="biab-appointment__label">Date</span>
475
+ <input
476
+ class="biab-input"
477
+ type="date"
478
+ value={v.date ?? ""}
479
+ onInput$={(_, el) =>
480
+ void set({ ...v, date: (el as HTMLInputElement).value })
481
+ }
482
+ />
483
+ </label>
484
+ <label class="biab-appointment__field">
485
+ <span class="biab-appointment__label">Time</span>
486
+ <input
487
+ class="biab-input"
488
+ type="time"
489
+ value={v.time ?? ""}
490
+ onInput$={(_, el) =>
491
+ void set({ ...v, time: (el as HTMLInputElement).value })
492
+ }
493
+ />
494
+ </label>
495
+ </div>
496
+ );
497
+ }
498
+
499
+ case "slider": {
500
+ const min = field.sliderConfig?.min ?? 0;
501
+ const max = field.sliderConfig?.max ?? 100;
502
+ const step = field.sliderConfig?.step ?? 1;
503
+ const unit = field.sliderConfig?.unit;
504
+ const v = typeof value === "number" ? value : min;
505
+ const fmt = (n: number) =>
506
+ unit === "currency" ? `$${n}` : unit === "percent" ? `${n}%` : `${n}`;
507
+ return (
508
+ <div class="biab-slider">
509
+ <div class="biab-slider__scale">
510
+ <span>{fmt(min)}</span>
511
+ <span class="biab-slider__value">{fmt(v)}</span>
512
+ <span>{fmt(max)}</span>
513
+ </div>
514
+ <input
515
+ class="biab-slider__input"
516
+ type="range"
517
+ min={min}
518
+ max={max}
519
+ step={step}
520
+ value={v}
521
+ onInput$={(_, el) => void set(Number((el as HTMLInputElement).value))}
522
+ />
523
+ </div>
524
+ );
525
+ }
526
+
527
+ case "star_rating": {
528
+ const max = field.starRatingConfig?.maxStars ?? 5;
529
+ const v = typeof value === "number" ? value : 0;
530
+ return (
531
+ <div class="biab-stars">
532
+ {Array.from({ length: max }).map((_, i) => (
533
+ <button
534
+ key={`${field.id}-star-${i}`}
535
+ type="button"
536
+ class={classOf("biab-star", i < v && "biab-star--on")}
537
+ aria-label={`${i + 1} star${i === 0 ? "" : "s"}`}
538
+ onClick$={() => void set(i + 1)}
539
+ >
540
+
541
+ </button>
542
+ ))}
543
+ </div>
544
+ );
545
+ }
546
+
547
+ case "happiness_rating": {
548
+ const v = typeof value === "number" ? value : -1;
549
+ const faces = ["😞", "😕", "😐", "🙂", "😄"];
550
+ return (
551
+ <div class="biab-happiness">
552
+ {faces.map((emoji, i) => (
553
+ <button
554
+ key={emoji}
555
+ type="button"
556
+ class={classOf("biab-happiness__face", i === v && "biab-happiness__face--on")}
557
+ onClick$={() => void set(i)}
558
+ >
559
+ {emoji}
560
+ </button>
561
+ ))}
562
+ </div>
563
+ );
564
+ }
565
+
566
+ case "stepper": {
567
+ const v = typeof value === "number" ? value : 0;
568
+ return (
569
+ <div class="biab-stepper">
570
+ <button
571
+ type="button"
572
+ class="biab-stepper__btn biab-stepper__btn--dec"
573
+ onClick$={() => void set(v - 1)}
574
+ >
575
+
576
+ </button>
577
+ <span class="biab-stepper__value">{v}</span>
578
+ <button
579
+ type="button"
580
+ class="biab-stepper__btn biab-stepper__btn--inc"
581
+ onClick$={() => void set(v + 1)}
582
+ >
583
+ +
584
+ </button>
585
+ </div>
586
+ );
587
+ }
588
+
589
+ case "color_picker": {
590
+ const v = asString(value);
591
+ return (
592
+ <div class="biab-colors">
593
+ {(field.options ?? []).map((color) => (
594
+ <button
595
+ key={color}
596
+ type="button"
597
+ class={classOf("biab-color", v === color && "biab-color--on")}
598
+ style={{ backgroundColor: color }}
599
+ title={color}
600
+ aria-label={color}
601
+ onClick$={() => void set(color)}
602
+ />
603
+ ))}
604
+ </div>
605
+ );
606
+ }
607
+
608
+ case "address": {
609
+ const v = (value && typeof value === "object" ? value : {}) as Record<
610
+ string,
611
+ string
612
+ >;
613
+ // Per-subfield autofill tokens (street/line2/city/state/zip), suppressed
614
+ // to "off" when autofill is off for this field (global switch or override).
615
+ const acOff = autoComplete === "off";
616
+ const part = (
617
+ key: string,
618
+ label: string,
619
+ ac: string,
620
+ ) => (
621
+ <label class={classOf("biab-address__field", `biab-address__field--${key}`)}>
622
+ <span class="biab-address__label">{label}</span>
623
+ <input
624
+ class="biab-input"
625
+ value={v[key] ?? ""}
626
+ autoComplete={acOff ? "off" : ac}
627
+ onInput$={(_, el) =>
628
+ void set({ ...v, [key]: (el as HTMLInputElement).value })
629
+ }
630
+ />
631
+ </label>
632
+ );
633
+ return (
634
+ <div class="biab-address">
635
+ {part("line1", "Address Line 1", "street-address")}
636
+ {part("line2", "Address Line 2", "address-line2")}
637
+ {part("city", "City", "address-level2")}
638
+ {part("state", "State", "address-level1")}
639
+ {part("zip", "Zip", "postal-code")}
640
+ </div>
641
+ );
642
+ }
643
+
644
+ case "file_upload": {
645
+ const files = Array.isArray(value)
646
+ ? (value as Array<{ name: string; size: number; url: string }>)
647
+ : [];
648
+ const maxFiles = field.fileConfig?.maxFiles ?? 1;
649
+ const allowed = field.fileConfig?.allowedTypes;
650
+ const accept = allowed && allowed.length > 0 ? allowed.join(",") : undefined;
651
+ const remaining = Math.max(maxFiles - files.length, 0);
652
+ const uploading = state.snap?.uploading === true;
653
+ return (
654
+ <div class="biab-file">
655
+ <input
656
+ class="biab-file__input"
657
+ type="file"
658
+ multiple={maxFiles > 1}
659
+ accept={accept}
660
+ disabled={uploading || remaining === 0}
661
+ onChange$={async (_, el) => {
662
+ const picked = (el as HTMLInputElement).files;
663
+ const ctrl = controllerSig.value;
664
+ if (!picked || picked.length === 0 || !ctrl) return;
665
+ const list = Array.from(picked).slice(0, remaining);
666
+ for (const file of list) {
667
+ try {
668
+ if (uploadFn) {
669
+ // SSR / no-client path: upload via the consumer's `server$`
670
+ // delegate (bearer key stays server-side), then append the
671
+ // returned FormFileValue to this field's value array.
672
+ const uploaded = await uploadFn(field.id, file);
673
+ const existing = Array.isArray(
674
+ ctrl.snapshot().values[field.id],
675
+ )
676
+ ? (ctrl.snapshot().values[field.id] as unknown[])
677
+ : [];
678
+ ctrl.setValue(field.id, [...existing, uploaded]);
679
+ } else {
680
+ // Client path: the controller uploads via its own SDK client.
681
+ await ctrl.uploadFile(field.id, file, {
682
+ filename: file.name,
683
+ });
684
+ }
685
+ } catch {
686
+ /* controller surfaces upload state; swallow per-file failure */
687
+ }
688
+ }
689
+ (el as HTMLInputElement).value = "";
690
+ }}
691
+ />
692
+ {uploading ? <span class="biab-file__status">Uploading…</span> : null}
693
+ {files.length > 0 ? (
694
+ <ul class="biab-file__list">
695
+ {files.map((f, idx) => (
696
+ <li class="biab-file__item" key={`${f.url}-${idx}`}>
697
+ <span class="biab-file__name">{f.name}</span>
698
+ <span class="biab-file__size">
699
+ {Math.round(f.size / 1024)} KB
700
+ </span>
701
+ </li>
702
+ ))}
703
+ </ul>
704
+ ) : null}
705
+ </div>
706
+ );
707
+ }
708
+
709
+ case "phone":
710
+ return (
711
+ <input
712
+ class="biab-input biab-phone"
713
+ type="tel"
714
+ value={asString(value)}
715
+ placeholder={
716
+ field.placeholder ||
717
+ (field.phoneConfig?.international
718
+ ? "+1 (555) 000-0000"
719
+ : "(555) 000-0000")
720
+ }
721
+ autoComplete={autoComplete}
722
+ onInput$={(_, el) => void set((el as HTMLInputElement).value)}
723
+ />
724
+ );
725
+
726
+ case "email":
727
+ return (
728
+ <input
729
+ class="biab-input biab-email"
730
+ type="email"
731
+ value={asString(value)}
732
+ placeholder={field.placeholder || "you@example.com"}
733
+ autoComplete={autoComplete}
734
+ onInput$={(_, el) => void set((el as HTMLInputElement).value)}
735
+ />
736
+ );
737
+
738
+ case "number":
739
+ return (
740
+ <input
741
+ class="biab-input biab-number"
742
+ type="number"
743
+ value={typeof value === "number" ? value : asString(value)}
744
+ placeholder={field.placeholder || ""}
745
+ onInput$={(_, el) => {
746
+ const raw = (el as HTMLInputElement).value;
747
+ void set(raw === "" ? "" : Number(raw));
748
+ }}
749
+ />
750
+ );
751
+
752
+ default:
753
+ return (
754
+ <input
755
+ class="biab-input biab-text"
756
+ type="text"
757
+ value={asString(value)}
758
+ placeholder={field.placeholder || ""}
759
+ autoComplete={autoComplete}
760
+ onInput$={(_, el) => void set((el as HTMLInputElement).value)}
761
+ />
762
+ );
763
+ }
764
+ });
765
+
766
+ /**
767
+ * One field "row": the `biab-field` wrapper with stable data-attrs + error /
768
+ * help text, mirroring the dashboard `FieldRow`.
769
+ */
770
+ const BiabFieldRow = component$<{
771
+ field: FormCoreField;
772
+ controllerSig: ControllerRef;
773
+ state: SnapshotState;
774
+ uploadFn?: UploadDelegate;
775
+ acConfig: AutocompleteConfig;
776
+ }>(({ field, controllerSig, state, uploadFn, acConfig }) => {
777
+ const showLabel = !NO_LABEL_TYPES.has(field.type);
778
+ const error = state.snap?.errors?.[field.id];
779
+ return (
780
+ <div
781
+ class={classOf(
782
+ "biab-field",
783
+ `biab-field--${field.type}`,
784
+ field.required && "biab-field--required",
785
+ error && "biab-field--error",
786
+ )}
787
+ data-biab-field={fieldCssKey(field)}
788
+ data-biab-type={field.type}
789
+ >
790
+ {showLabel ? (
791
+ <label class="biab-field__label">
792
+ {field.label || ""}
793
+ {field.required ? <span class="biab-field__required">*</span> : null}
794
+ </label>
795
+ ) : null}
796
+ <BiabFieldInput
797
+ field={field}
798
+ controllerSig={controllerSig}
799
+ state={state}
800
+ uploadFn={uploadFn}
801
+ acConfig={acConfig}
802
+ />
803
+ {error ? <p class="biab-field__error">{error.message}</p> : null}
804
+ {field.helpText ? (
805
+ <p class="biab-field__help">{field.helpText}</p>
806
+ ) : null}
807
+ </div>
808
+ );
809
+ });
810
+
811
+ /**
812
+ * Recursive render of the nested field tree — mirrors `FormPreview.RenderTree`:
813
+ * `input_group` (with concurrent-reveal gating + optional group label),
814
+ * `condition_block` (skipped when its rule is false), `or_condition` (two sides
815
+ * with an OR divider), `flex_row` (inline row), else a `BiabFieldRow`.
816
+ */
817
+ const BiabRenderTree = component$<{
818
+ fields: FormCoreField[];
819
+ controllerSig: ControllerRef;
820
+ state: SnapshotState;
821
+ uploadFn?: UploadDelegate;
822
+ acConfig: AutocompleteConfig;
823
+ flat?: boolean;
824
+ }>(({ fields, controllerSig, state, uploadFn, acConfig, flat }) => {
825
+ const data = state.snap?.values ?? {};
826
+ return (
827
+ <div class="biab-tree">
828
+ {fields.map((f) => {
829
+ if (f.type === "input_group") {
830
+ const children = (f.children ?? []) as FormCoreField[];
831
+ const concurrent = f.concurrentReveal === true;
832
+ const showGroupLabel = f.showGroupLabel === true;
833
+ const revealCount = concurrent
834
+ ? concurrentRevealCount(children, data)
835
+ : children.length;
836
+ const shown = children.slice(0, revealCount);
837
+ return (
838
+ <div
839
+ key={f.id}
840
+ class={classOf("biab-group", flat && "biab-group--flat")}
841
+ data-biab-group={fieldCssKey(f)}
842
+ >
843
+ {f.label && showGroupLabel ? (
844
+ <div class="biab-group__label">{f.label}</div>
845
+ ) : null}
846
+ <BiabRenderTree
847
+ fields={shown}
848
+ controllerSig={controllerSig}
849
+ state={state}
850
+ uploadFn={uploadFn}
851
+ acConfig={acConfig}
852
+ />
853
+ </div>
854
+ );
855
+ }
856
+
857
+ if (f.type === "condition_block") {
858
+ if (!evalCondition(f.conditionRule, data)) return null;
859
+ return (
860
+ <BiabRenderTree
861
+ key={f.id}
862
+ fields={(f.children ?? []) as FormCoreField[]}
863
+ controllerSig={controllerSig}
864
+ state={state}
865
+ uploadFn={uploadFn}
866
+ acConfig={acConfig}
867
+ />
868
+ );
869
+ }
870
+
871
+ if (f.type === "or_condition") {
872
+ const left = f.children?.[0];
873
+ const right = f.children?.[1];
874
+ const error = state.snap?.errors?.[f.id];
875
+ return (
876
+ <div
877
+ key={f.id}
878
+ class={classOf("biab-or", error && "biab-or--error")}
879
+ data-biab-or={fieldCssKey(f)}
880
+ >
881
+ {f.label ? (
882
+ <div class="biab-or__label">
883
+ {f.label}
884
+ <span class="biab-or__hint">Fill at least one side</span>
885
+ </div>
886
+ ) : null}
887
+ <div class="biab-or__grid">
888
+ <div class="biab-or__side biab-or__side--left">
889
+ <div class="biab-or__side-label">
890
+ {f.orConditionConfig?.leftLabel || "Option A"}
891
+ </div>
892
+ {left ? (
893
+ <BiabRenderTree
894
+ fields={[left]}
895
+ controllerSig={controllerSig}
896
+ state={state}
897
+ uploadFn={uploadFn}
898
+ acConfig={acConfig}
899
+ />
900
+ ) : null}
901
+ </div>
902
+ <div class="biab-or__divider">
903
+ <span class="biab-or__badge">OR</span>
904
+ </div>
905
+ <div class="biab-or__side biab-or__side--right">
906
+ <div class="biab-or__side-label">
907
+ {f.orConditionConfig?.rightLabel || "Option B"}
908
+ </div>
909
+ {right ? (
910
+ <BiabRenderTree
911
+ fields={[right]}
912
+ controllerSig={controllerSig}
913
+ state={state}
914
+ uploadFn={uploadFn}
915
+ acConfig={acConfig}
916
+ />
917
+ ) : null}
918
+ </div>
919
+ </div>
920
+ {error ? (
921
+ <p class="biab-or__error">Fill in at least one side to continue.</p>
922
+ ) : null}
923
+ </div>
924
+ );
925
+ }
926
+
927
+ if (f.type === "flex_row") {
928
+ const children = (f.children ?? []) as FormCoreField[];
929
+ return (
930
+ <div key={f.id} class="biab-flex-row">
931
+ {children.map((child) => (
932
+ <div class="biab-flex-row__cell" key={child.id}>
933
+ <BiabFieldRow
934
+ field={child}
935
+ controllerSig={controllerSig}
936
+ state={state}
937
+ uploadFn={uploadFn}
938
+ acConfig={acConfig}
939
+ />
940
+ </div>
941
+ ))}
942
+ </div>
943
+ );
944
+ }
945
+
946
+ return (
947
+ <BiabFieldRow
948
+ key={f.id}
949
+ field={f}
950
+ controllerSig={controllerSig}
951
+ state={state}
952
+ uploadFn={uploadFn}
953
+ acConfig={acConfig}
954
+ />
955
+ );
956
+ })}
957
+ </div>
958
+ );
959
+ });
960
+
961
+ // ─────────────────────────────────────────────────────────────────────────────
962
+ // <BiabForm> — the drop-in.
963
+ // ─────────────────────────────────────────────────────────────────────────────
964
+
965
+ export const BiabForm = component$<BiabFormProps>((props) => {
966
+ // The controller lives only on the client (it's a non-serializable closure).
967
+ // `noSerialize()` tells Qwik to drop it from the resumable state instead of
968
+ // trying (and failing) to serialize the closure; it is re-created in the
969
+ // client-only `useVisibleTask$` below.
970
+ const controllerSig = useSignal<NoSerialize<FormController> | undefined>(
971
+ undefined,
972
+ );
973
+ // Serializable mirror of the controller's snapshot — drives every render.
974
+ const state = useStore<SnapshotState>(
975
+ {
976
+ // Seed from the pre-fetched schema so SSR renders the fields immediately
977
+ // (no flicker). The controller re-adopts this same schema on the client.
978
+ snap: seedSnapshot(props.schema),
979
+ },
980
+ { deep: true },
981
+ );
982
+
983
+ // eslint-disable-next-line qwik/no-use-visible-task
984
+ useVisibleTask$(({ cleanup }) => {
985
+ // Build the controller client-side. `submit$` / `uploadFile$` keep the
986
+ // bearer key server-side; the controller's own client is the fallback when
987
+ // a `client` was passed directly.
988
+ const submitOptions: FormSubmitOptions = {
989
+ ...props.submitOptions,
990
+ ...(props.source ? { source: props.source } : {}),
991
+ ...(props.dryRun ? { dryRun: true } : {}),
992
+ };
993
+
994
+ const controller = createFormController({
995
+ ...(props.client ? { client: props.client } : {}),
996
+ ...(props.schema ? { schema: props.schema } : {}),
997
+ ...(props.slug ? { slug: props.slug } : {}),
998
+ submitOptions,
999
+ onSuccess: (result) => {
1000
+ void props.onSuccess$?.(result);
1001
+ },
1002
+ onError: (result) => {
1003
+ void props.onError$?.(result);
1004
+ },
1005
+ });
1006
+ controllerSig.value = noSerialize(controller);
1007
+
1008
+ // Mirror the snapshot into the reactive store on every notify.
1009
+ const unsubscribe = controller.subscribe((snap) => {
1010
+ state.snap = snap;
1011
+ });
1012
+ // Load (adopts the pre-fetched schema synchronously, or fetches via client).
1013
+ void controller.load().then(() => {
1014
+ state.snap = controller.snapshot();
1015
+ });
1016
+
1017
+ cleanup(() => {
1018
+ unsubscribe();
1019
+ controller.destroy();
1020
+ });
1021
+ });
1022
+
1023
+ const rootClass = classOf("biab-form", props.class, props.className);
1024
+
1025
+ // HTML autofill config threaded to every field input. A plain serializable
1026
+ // object (Qwik-friendly): the global switch + per-field overrides. Mirrors the
1027
+ // React adapter; resolution lives in forms-core/autocomplete.ts.
1028
+ const acConfig: AutocompleteConfig = {
1029
+ ...(props.autoComplete !== undefined ? { enabled: props.autoComplete } : {}),
1030
+ ...(props.fieldAutoComplete ? { overrides: props.fieldAutoComplete } : {}),
1031
+ };
1032
+
1033
+ // ── Submit handler — routes through `submit$` (server) when present, else the
1034
+ // controller's own SDK client. Validation + re-keying happen in the core. ──
1035
+ const handleSubmit = $(async () => {
1036
+ const controller = controllerSig.value;
1037
+ if (!controller) return;
1038
+
1039
+ if (props.submit$) {
1040
+ // SSR path: validate + re-key in the core, delegate the round-trip to
1041
+ // the consumer's `server$`. `validate()` publishes inline errors; the
1042
+ // core's `toSubmissionPayload` re-keys id → output key.
1043
+ const missing = controller.validate();
1044
+ if (missing.length > 0) return;
1045
+ const snap = controller.snapshot();
1046
+ // Re-key for the wire via the SAME mapping the core uses on submit
1047
+ // (id → published output key; the server also accepts the legacy id).
1048
+ const payload = toSubmissionPayload(snap.fields, snap.values);
1049
+ void props.onSubmit$?.(payload);
1050
+ const mergedOptions: FormSubmitOptions = {
1051
+ ...props.submitOptions,
1052
+ ...(props.source ? { source: props.source } : {}),
1053
+ ...(props.dryRun ? { dryRun: true } : {}),
1054
+ };
1055
+ const result = await props.submit$(payload, mergedOptions);
1056
+ // Feed the server result back through the store for inline rendering.
1057
+ state.snap = { ...controller.snapshot(), result, submitted: result.ok };
1058
+ if (result.ok) void props.onSuccess$?.(result);
1059
+ else void props.onError$?.(result);
1060
+ return;
1061
+ }
1062
+
1063
+ // Client path: the controller owns validation + the round-trip via its SDK.
1064
+ await controller.submit();
1065
+ });
1066
+
1067
+ const snap = state.snap;
1068
+ if (!snap || snap.status === "loading" || snap.status === "idle") {
1069
+ return (
1070
+ <div class={rootClass} data-biab-status="loading">
1071
+ <Slot name="loading" />
1072
+ </div>
1073
+ );
1074
+ }
1075
+ if (snap.status === "error") {
1076
+ return (
1077
+ <div class={rootClass} data-biab-status="error">
1078
+ <Slot name="error" />
1079
+ <p class="biab-form__load-error">
1080
+ {snap.loadError ?? "Could not load the form."}
1081
+ </p>
1082
+ </div>
1083
+ );
1084
+ }
1085
+
1086
+ const settings = snap.settings;
1087
+ const consecutive = settings.isConsecutive;
1088
+ const showSteps =
1089
+ consecutive &&
1090
+ settings.showProgress &&
1091
+ settings.progressStyle !== "bar" &&
1092
+ snap.stepCount > 1;
1093
+ const showBar =
1094
+ consecutive &&
1095
+ settings.showProgress &&
1096
+ settings.progressStyle === "bar" &&
1097
+ snap.stepCount > 1;
1098
+
1099
+ // Animation entrance class (per dashboard `entranceClass`). "scroll" is left
1100
+ // to the consumer's CSS via the `biab-form--anim-scroll` hook.
1101
+ const animClass =
1102
+ settings.animation === "none" ? "" : `biab-form--anim-${settings.animation}`;
1103
+ const dirClass = snap.forward ? "biab-form--forward" : "biab-form--back";
1104
+
1105
+ if (snap.submitted) {
1106
+ return (
1107
+ <div class={classOf(rootClass, "biab-form--submitted")} data-biab-status="submitted">
1108
+ <Slot name="success" />
1109
+ </div>
1110
+ );
1111
+ }
1112
+
1113
+ return (
1114
+ <div class={rootClass} data-biab-status="ready" data-biab-animation={settings.animation}>
1115
+ {snap.orgIcon ? (
1116
+ <img class="biab-form__org-icon" src={snap.orgIcon} alt="" />
1117
+ ) : null}
1118
+
1119
+ {snap.title ? <h2 class="biab-form__title">{snap.title}</h2> : null}
1120
+ {snap.description ? (
1121
+ <p class="biab-form__description">{snap.description}</p>
1122
+ ) : null}
1123
+
1124
+ {showSteps ? (
1125
+ <div class="biab-progress biab-progress--steps">
1126
+ <div class="biab-progress__heading">Form Progress</div>
1127
+ <ol class="biab-progress__steps">
1128
+ {snap.steps.map((s, i) => {
1129
+ const head = s[0];
1130
+ const title = head?.stepTitle || head?.label || `Step ${i + 1}`;
1131
+ const done = i < snap.step;
1132
+ const current = i === snap.step;
1133
+ return (
1134
+ <li
1135
+ key={`biab-step-${i}`}
1136
+ class={classOf(
1137
+ "biab-progress__step",
1138
+ current && "biab-progress__step--current",
1139
+ done && "biab-progress__step--done",
1140
+ )}
1141
+ >
1142
+ <span class="biab-progress__marker">{done ? "✓" : i + 1}</span>
1143
+ <span class="biab-progress__title">{title}</span>
1144
+ </li>
1145
+ );
1146
+ })}
1147
+ </ol>
1148
+ </div>
1149
+ ) : null}
1150
+
1151
+ {showBar ? (
1152
+ <div class="biab-progress biab-progress--bar">
1153
+ <span class="biab-progress__count">
1154
+ {snap.step + 1} / {snap.stepCount}
1155
+ </span>
1156
+ <div class="biab-progress__track">
1157
+ <div
1158
+ class="biab-progress__fill"
1159
+ style={{ width: `${snap.progressPercent}%` }}
1160
+ />
1161
+ </div>
1162
+ </div>
1163
+ ) : null}
1164
+
1165
+ {snap.errorList.length > 0 ? (
1166
+ <div class="biab-form__errors" role="alert">
1167
+ <div class="biab-form__errors-heading">
1168
+ Please complete the highlighted required field
1169
+ {snap.errorList.length === 1 ? "" : "s"}.
1170
+ </div>
1171
+ <div class="biab-form__errors-list">
1172
+ {snap.errorList.map((e) => e.label).join(", ")}
1173
+ </div>
1174
+ </div>
1175
+ ) : null}
1176
+
1177
+ <div class={classOf("biab-form__body", animClass, dirClass)} key={`biab-body-${snap.step}`}>
1178
+ <BiabRenderTree
1179
+ fields={snap.visibleFields}
1180
+ controllerSig={controllerSig}
1181
+ state={state}
1182
+ uploadFn={props.uploadFile$}
1183
+ acConfig={acConfig}
1184
+ flat={consecutive}
1185
+ />
1186
+ </div>
1187
+
1188
+ {consecutive && snap.stepCount > 1 ? (
1189
+ <div class="biab-nav">
1190
+ <button
1191
+ type="button"
1192
+ class="biab-btn biab-btn--back"
1193
+ disabled={snap.isFirstStep}
1194
+ onClick$={() => controllerSig.value?.back()}
1195
+ >
1196
+ Back
1197
+ </button>
1198
+ {snap.isLastStep ? (
1199
+ <button
1200
+ type="button"
1201
+ class="biab-btn biab-btn--submit"
1202
+ disabled={snap.submitting || snap.uploading}
1203
+ onClick$={handleSubmit}
1204
+ >
1205
+ {snap.submitting ? "Sending…" : props.submitLabel || "Submit"}
1206
+ </button>
1207
+ ) : (
1208
+ <button
1209
+ type="button"
1210
+ class="biab-btn biab-btn--next"
1211
+ disabled={!snap.canAdvance}
1212
+ onClick$={() => controllerSig.value?.next()}
1213
+ >
1214
+ Next
1215
+ </button>
1216
+ )}
1217
+ </div>
1218
+ ) : (
1219
+ <div class="biab-nav">
1220
+ <button
1221
+ type="button"
1222
+ class="biab-btn biab-btn--submit"
1223
+ disabled={snap.submitting || snap.uploading}
1224
+ onClick$={handleSubmit}
1225
+ >
1226
+ {snap.submitting ? "Sending…" : props.submitLabel || "Submit"}
1227
+ </button>
1228
+ </div>
1229
+ )}
1230
+ </div>
1231
+ );
1232
+ });
1233
+
1234
+ export default BiabForm;