@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/dist/react.js ADDED
@@ -0,0 +1,2418 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, Fragment, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
3
+ import { BiabDevClient } from "./client.js";
4
+ import { buildAcceptAttr, concurrentRevealCount, createFormController, evalCondition, fetchIconNames, filterIconNames, flattenValueFields, formatCalcValue, iconUrl, isPendingFileValue, resolveFieldAutocomplete, shouldSearchOptions, validateFile, } from "./forms-core/index.js";
5
+ import { baseUnitFor, convertToBase, formatMeasurement, isMeasurementCategory, unitsForCategory, } from "./forms-core/units.js";
6
+ import { US_STATES } from "./forms-core/us-states.js";
7
+ import { createBiabClient } from "./sdk.js";
8
+ import { resolveSocialProfiles } from "./socials.js";
9
+ function useEmbedSession(loader, options) {
10
+ const { apiKey, baseUrl, refreshLeadMs = 30_000 } = options;
11
+ const client = useMemo(() => new BiabDevClient({ apiKey, baseUrl }), [apiKey, baseUrl]);
12
+ const [state, setState] = useState({ status: "idle" });
13
+ // `loader` is recreated per surface; depending on it would refetch on every
14
+ // render, so it's intentionally omitted from the deps below.
15
+ // biome-ignore lint/correctness/useExhaustiveDependencies: loader omitted by design
16
+ const refresh = useCallback(async () => {
17
+ setState({ status: "loading" });
18
+ try {
19
+ const session = await loader(client);
20
+ setState({ status: "ready", session });
21
+ }
22
+ catch (error) {
23
+ setState({
24
+ status: "error",
25
+ error: error instanceof Error ? error : new Error("Unknown session error"),
26
+ });
27
+ }
28
+ }, [client]);
29
+ useEffect(() => {
30
+ void refresh();
31
+ }, [refresh]);
32
+ useEffect(() => {
33
+ if (state.status !== "ready")
34
+ return;
35
+ const expiresAt = Date.parse(state.session.expiresAt);
36
+ if (Number.isNaN(expiresAt))
37
+ return;
38
+ const delay = Math.max(1_000, expiresAt - Date.now() - refreshLeadMs);
39
+ const timer = setTimeout(() => {
40
+ void refresh();
41
+ }, delay);
42
+ return () => clearTimeout(timer);
43
+ }, [state, refresh, refreshLeadMs]);
44
+ return { client, state, refresh };
45
+ }
46
+ function useEmbedMessages(iframeRef, handler) {
47
+ useEffect(() => {
48
+ const onMessage = (e) => {
49
+ if (!e.data || typeof e.data !== "object")
50
+ return;
51
+ if (e.data.kind !== "biab")
52
+ return;
53
+ if (iframeRef.current &&
54
+ e.source !== null &&
55
+ e.source !== iframeRef.current.contentWindow)
56
+ return;
57
+ const { kind, ...rest } = e.data;
58
+ void kind;
59
+ handler(rest);
60
+ };
61
+ window.addEventListener("message", onMessage);
62
+ return () => window.removeEventListener("message", onMessage);
63
+ }, [handler, iframeRef]);
64
+ }
65
+ // Asserted-compat with the underlying DashboardSessionResponse so the public
66
+ // `useDashboardSession` shape doesn't churn for existing callers.
67
+ const _dashboardCompat = true;
68
+ void _dashboardCompat;
69
+ const DashboardContext = createContext(null);
70
+ export function DashboardProvider({ apiKey, baseUrl, children, refreshLeadMs, }) {
71
+ if (!apiKey) {
72
+ throw new Error("<DashboardProvider> requires an `apiKey`.");
73
+ }
74
+ if (!baseUrl) {
75
+ throw new Error("<DashboardProvider> requires a `baseUrl`.");
76
+ }
77
+ const { state, refresh } = useEmbedSession((client) => client.dashboard.createSession(), { apiKey, baseUrl, refreshLeadMs });
78
+ const value = useMemo(() => ({ state, refresh }), [state, refresh]);
79
+ return (_jsx(DashboardContext.Provider, { value: value, children: children }));
80
+ }
81
+ export function useDashboardSession() {
82
+ const ctx = useContext(DashboardContext);
83
+ if (!ctx) {
84
+ throw new Error("useDashboardSession must be used inside <DashboardProvider>.");
85
+ }
86
+ return ctx;
87
+ }
88
+ const defaultIframeStyle = {
89
+ width: "100%",
90
+ height: "100%",
91
+ minHeight: "600px",
92
+ border: "0",
93
+ display: "block",
94
+ };
95
+ export function Dashboard({ className, style, title = "Business In A Box Dashboard", loadingFallback, errorFallback, } = {}) {
96
+ const { state, refresh } = useDashboardSession();
97
+ if (state.status === "error") {
98
+ if (errorFallback)
99
+ return _jsx(_Fragment, { children: errorFallback(state.error, () => void refresh()) });
100
+ return (_jsxs("div", { className: className, style: { padding: 16, ...style }, children: ["Dashboard unavailable: ", state.error.message] }));
101
+ }
102
+ if (state.status !== "ready") {
103
+ return (_jsx(_Fragment, { children: loadingFallback ?? (_jsx("div", { className: className, style: { padding: 16, ...style }, children: "Loading dashboard\u2026" })) }));
104
+ }
105
+ return (_jsx("iframe", { className: className, sandbox: "allow-scripts allow-forms allow-same-origin allow-popups", src: state.session.url, style: { ...defaultIframeStyle, ...style }, title: title }));
106
+ }
107
+ function EmbedIframe({ state, refresh, title, className, style, loadingFallback, errorFallback, onEvent, extraSandbox, }) {
108
+ const iframeRef = useRef(null);
109
+ const onEventRef = useRef(onEvent);
110
+ useEffect(() => {
111
+ onEventRef.current = onEvent;
112
+ }, [onEvent]);
113
+ useEmbedMessages(iframeRef, (event) => {
114
+ onEventRef.current?.(event);
115
+ });
116
+ if (state.status === "error") {
117
+ if (errorFallback)
118
+ return _jsx(_Fragment, { children: errorFallback(state.error, () => void refresh()) });
119
+ return (_jsxs("div", { className: className, style: { padding: 16, ...style }, children: ["Embed unavailable: ", state.error.message] }));
120
+ }
121
+ if (state.status !== "ready") {
122
+ return (_jsx(_Fragment, { children: loadingFallback ?? (_jsx("div", { className: className, style: { padding: 16, ...style }, children: "Loading\u2026" })) }));
123
+ }
124
+ const sandbox = `allow-scripts allow-forms allow-same-origin allow-popups${extraSandbox ? ` ${extraSandbox}` : ""}`;
125
+ return (_jsx("iframe", { className: className, ref: iframeRef, sandbox: sandbox, src: state.session.url, style: { ...defaultIframeStyle, ...style }, title: title }));
126
+ }
127
+ export function Storefront({ apiKey, baseUrl, productId, className, style, title = "Storefront", loadingFallback, errorFallback, onEvent, onCartChange, refreshLeadMs, }) {
128
+ const input = useMemo(() => (productId ? { productId } : undefined), [productId]);
129
+ const { state, refresh } = useEmbedSession((client) => client.storefront.createSession(input), { apiKey, baseUrl, refreshLeadMs });
130
+ const handleEvent = useCallback((event) => {
131
+ onEvent?.(event);
132
+ if (event.type === "biab:cart:update") {
133
+ onCartChange?.(event.count, event.subtotalCents);
134
+ }
135
+ }, [onEvent, onCartChange]);
136
+ return (_jsx(EmbedIframe, { className: className, errorFallback: errorFallback, loadingFallback: loadingFallback, onEvent: handleEvent, refresh: refresh, state: state, style: style, title: title }));
137
+ }
138
+ export function Cart({ apiKey, baseUrl, className, style, title = "Cart", loadingFallback, errorFallback, onEvent, onCartChange, refreshLeadMs, }) {
139
+ const { state, refresh } = useEmbedSession((client) => client.cart.createSession(), { apiKey, baseUrl, refreshLeadMs });
140
+ const handleEvent = useCallback((event) => {
141
+ onEvent?.(event);
142
+ if (event.type === "biab:cart:update") {
143
+ onCartChange?.(event.count, event.subtotalCents);
144
+ }
145
+ }, [onEvent, onCartChange]);
146
+ return (_jsx(EmbedIframe, { className: className, errorFallback: errorFallback, loadingFallback: loadingFallback, onEvent: handleEvent, refresh: refresh, state: state, style: style, title: title }));
147
+ }
148
+ export function Checkout(props) {
149
+ const { apiKey, baseUrl, cart, mode = "redirect", className, style, title = "Checkout", children, loadingFallback, errorFallback, onEvent, onSuccess, onCancel, onError, } = props;
150
+ const client = useMemo(() => new BiabDevClient({ apiKey, baseUrl }), [apiKey, baseUrl]);
151
+ const [state, setState] = useState({
152
+ status: "idle",
153
+ });
154
+ const start = useCallback(async () => {
155
+ setState({ status: "loading" });
156
+ try {
157
+ const session = await client.checkout.createSession(cart);
158
+ setState({ status: "ready", session });
159
+ if (mode === "redirect") {
160
+ window.location.href = session.stripeUrl;
161
+ }
162
+ }
163
+ catch (error) {
164
+ const err = error instanceof Error ? error : new Error("Checkout failed");
165
+ setState({ status: "error", error: err });
166
+ onError?.(err);
167
+ }
168
+ }, [client, cart, mode, onError]);
169
+ const handleEvent = useCallback((event) => {
170
+ onEvent?.(event);
171
+ if (event.type === "biab:checkout:success")
172
+ onSuccess?.(event.sessionId);
173
+ else if (event.type === "biab:checkout:cancel")
174
+ onCancel?.(event.sessionId);
175
+ else if (event.type === "biab:checkout:error")
176
+ onError?.(new Error(event.message));
177
+ }, [onEvent, onSuccess, onCancel, onError]);
178
+ // Lazily create the session once it's idle (iframe mode). Declared BEFORE the
179
+ // redirect-mode early return so the hook order stays stable across renders
180
+ // (rules of hooks); the mode guard keeps redirect mode from auto-starting.
181
+ useEffect(() => {
182
+ if (mode !== "redirect" && state.status === "idle")
183
+ void start();
184
+ }, [mode, state.status, start]);
185
+ if (mode === "redirect") {
186
+ return (_jsx("button", { className: className, disabled: state.status === "loading", onClick: () => void start(), style: style, type: "button", children: children ?? (state.status === "loading" ? "Loading…" : "Checkout") }));
187
+ }
188
+ return (_jsx(EmbedIframe, { className: className, errorFallback: errorFallback, extraSandbox: "allow-top-navigation", loadingFallback: loadingFallback, onEvent: handleEvent, refresh: start, state: state, style: style, title: title }));
189
+ }
190
+ export function CheckoutResult(props) {
191
+ const { apiKey, baseUrl, orgSlug, sessionIdParam = "session_id", render, } = props;
192
+ const [state, setState] = useState({ status: "loading" });
193
+ useEffect(() => {
194
+ const url = new URL(window.location.href);
195
+ const sessionId = url.searchParams.get(sessionIdParam);
196
+ if (!sessionId) {
197
+ setState({ status: "error", error: new Error("Missing session id.") });
198
+ return;
199
+ }
200
+ const client = new BiabDevClient({ apiKey, baseUrl });
201
+ // We hit the *public* tRPC endpoint via fetch — no API key required.
202
+ const trpcUrl = baseUrl.replace(/\/api\/package\/v1\/?$/, "");
203
+ const requestUrl = `${trpcUrl}/api/trpc/store.checkoutSessionStatus?input=${encodeURIComponent(JSON.stringify({ json: { orgSlug, sessionId } }))}`;
204
+ // Defensive: reject misconfigured baseUrl that resolves to a non-http(s)
205
+ // scheme (e.g. `javascript:` slipping in through SDK config).
206
+ const parsed = (() => {
207
+ try {
208
+ return new URL(requestUrl);
209
+ }
210
+ catch {
211
+ return null;
212
+ }
213
+ })();
214
+ if (!parsed ||
215
+ (parsed.protocol !== "https:" && parsed.protocol !== "http:")) {
216
+ setState({
217
+ status: "error",
218
+ error: new Error(`Invalid baseUrl: ${baseUrl}`),
219
+ });
220
+ return;
221
+ }
222
+ fetch(parsed.toString())
223
+ .then((r) => r.json())
224
+ .then((data) => {
225
+ const result = data?.result?.data?.json ?? data?.result?.data;
226
+ if (!result)
227
+ throw new Error("Empty checkout-status response.");
228
+ setState({
229
+ status: result.paymentStatus === "paid" ? "paid" : "unpaid",
230
+ amountTotalCents: result.amountTotalCents,
231
+ currency: result.currency,
232
+ customerEmail: result.customerEmail,
233
+ });
234
+ })
235
+ .catch((err) => {
236
+ setState({
237
+ status: "error",
238
+ error: err instanceof Error ? err : new Error(String(err)),
239
+ });
240
+ });
241
+ void client; // silence unused-warning if fetch path changes later
242
+ }, [apiKey, baseUrl, orgSlug, sessionIdParam]);
243
+ if (render)
244
+ return _jsx(_Fragment, { children: render(state) });
245
+ if (state.status === "loading")
246
+ return _jsx("p", { children: "Loading order\u2026" });
247
+ if (state.status === "error")
248
+ return _jsxs("p", { children: ["Couldn't load order: ", state.error?.message] });
249
+ if (state.status === "paid")
250
+ return (_jsxs("p", { children: ["Thank you! Receipt sent to ", state.customerEmail ?? "your email", "."] }));
251
+ return _jsx("p", { children: "Payment status: pending. We'll email you when it finalizes." });
252
+ }
253
+ export function Chatbot({ apiKey, baseUrl, className, style, title = "Assistant", loadingFallback, errorFallback, onEvent, onAssistantMessage, refreshLeadMs, }) {
254
+ const { state, refresh } = useEmbedSession((client) => client.chatbot.createSession(), { apiKey, baseUrl, refreshLeadMs });
255
+ const handleEvent = useCallback((event) => {
256
+ onEvent?.(event);
257
+ if (event.type === "biab:chatbot:assistant-message") {
258
+ onAssistantMessage?.(event.content, event.model);
259
+ }
260
+ }, [onEvent, onAssistantMessage]);
261
+ return (_jsx(EmbedIframe, { className: className, errorFallback: errorFallback, loadingFallback: loadingFallback, onEvent: handleEvent, refresh: refresh, state: state, style: style, title: title }));
262
+ }
263
+ /**
264
+ * Headless chat hook — for hosts that want to render their own chat UI but
265
+ * still drive every turn through the org's persisted chatbot config.
266
+ *
267
+ * Beyond `messages` and `send`, the hook tracks the latest `uiActions` (e.g.
268
+ * `show_form` — call {@link useChatbot}.getForm to fetch the definition and
269
+ * {@link useChatbot}.submitForm to POST a filled form back), and the latest
270
+ * `availability` snapshot (front-desk presence + wait-time hint) so the
271
+ * consumer can show or hide its "Talk to a person" CTA.
272
+ */
273
+ export function useChatbot(options) {
274
+ const { apiKey, baseUrl } = options;
275
+ const client = useMemo(() => new BiabDevClient({ apiKey, baseUrl }), [apiKey, baseUrl]);
276
+ const [messages, setMessages] = useState([]);
277
+ const [isPending, setIsPending] = useState(false);
278
+ const [error, setError] = useState(null);
279
+ const [latestUiActions, setLatestUiActions] = useState([]);
280
+ const [availability, setAvailability] = useState(null);
281
+ const send = useCallback(async (content) => {
282
+ const trimmed = content.trim();
283
+ if (!trimmed)
284
+ return null;
285
+ setError(null);
286
+ setIsPending(true);
287
+ const next = [
288
+ ...messages,
289
+ { role: "user", content: trimmed },
290
+ ];
291
+ setMessages(next);
292
+ try {
293
+ const input = { messages: next };
294
+ const reply = await client.chatbot.chat(input);
295
+ setMessages((prev) => [
296
+ ...prev,
297
+ { role: "assistant", content: reply.text },
298
+ ]);
299
+ setLatestUiActions(reply.uiActions ?? []);
300
+ if (reply.availability)
301
+ setAvailability(reply.availability);
302
+ return reply;
303
+ }
304
+ catch (err) {
305
+ const e = err instanceof Error ? err : new Error("Chat failed");
306
+ setError(e);
307
+ return null;
308
+ }
309
+ finally {
310
+ setIsPending(false);
311
+ }
312
+ }, [client, messages]);
313
+ const reset = useCallback(() => {
314
+ setMessages([]);
315
+ setError(null);
316
+ setLatestUiActions([]);
317
+ }, []);
318
+ const getConfig = useCallback(async () => client.chatbot.getConfig(), [client]);
319
+ const refreshAvailability = useCallback(async () => {
320
+ try {
321
+ const next = await client.chatbot.getAvailability();
322
+ setAvailability(next);
323
+ return next;
324
+ }
325
+ catch {
326
+ return null;
327
+ }
328
+ }, [client]);
329
+ const getForm = useCallback(async (slug) => {
330
+ try {
331
+ // Chat `show_form` flow → the chatbot path (Live + chatbotAccess).
332
+ return await client.chatbot.getChatForm(slug);
333
+ }
334
+ catch {
335
+ return null;
336
+ }
337
+ }, [client]);
338
+ const submitForm = useCallback(async (slug, input) => {
339
+ try {
340
+ const priorChat = messages.map((m, i) => ({
341
+ role: m.role,
342
+ content: m.content,
343
+ createdAt: i,
344
+ }));
345
+ // Chatbot submit carries the transcript → chatbot path.
346
+ return await client.chatbot.submitChatForm(slug, {
347
+ ...input,
348
+ priorChat: input.priorChat ?? priorChat,
349
+ });
350
+ }
351
+ catch (err) {
352
+ setError(err instanceof Error ? err : new Error("Submit failed"));
353
+ return null;
354
+ }
355
+ }, [client, messages]);
356
+ const uploadFile = useCallback((args) => client.chatbot.uploadFile(args), [client]);
357
+ return {
358
+ messages,
359
+ send,
360
+ reset,
361
+ isPending,
362
+ error,
363
+ getConfig,
364
+ latestUiActions,
365
+ availability,
366
+ refreshAvailability,
367
+ getForm,
368
+ submitForm,
369
+ uploadFile,
370
+ };
371
+ }
372
+ const STATIC_FIELD_TYPES = new Set([
373
+ "title",
374
+ "subtitle",
375
+ "separator",
376
+ "flex_row",
377
+ "input_group",
378
+ ]);
379
+ /**
380
+ * Structural-only inline styles. Anything visual (gap, padding, color,
381
+ * border, radius) is left for the consumer's `classNames` slots to set
382
+ * via their own Tailwind / CSS — these inline styles only enforce that
383
+ * the layout actually works (column flex, scrolling message list, etc.)
384
+ * and would otherwise be tedious to require from every consumer.
385
+ */
386
+ const inlineLayoutStyles = {
387
+ root: {
388
+ display: "flex",
389
+ flexDirection: "column",
390
+ minHeight: 0,
391
+ // `relative` so the jump-to-latest control can absolutely-position itself
392
+ // over the transcript.
393
+ position: "relative",
394
+ overflow: "hidden",
395
+ },
396
+ messageList: {
397
+ flex: 1,
398
+ overflowY: "auto",
399
+ display: "flex",
400
+ flexDirection: "column",
401
+ },
402
+ inputBar: { display: "flex" },
403
+ inputField: { flex: 1 },
404
+ formField: { display: "flex", flexDirection: "column" },
405
+ };
406
+ /**
407
+ * Opt-in default visual theme for {@link ChatbotInline}. Every rule is wrapped
408
+ * in `:where(…)` so it has **zero specificity** — any class a consumer passes
409
+ * via `classNames` (or their global CSS) overrides it without `!important`.
410
+ * Scoped to the `[data-biab-chatbot]` root (set only when `unstyled` is false),
411
+ * and targets the per-slot `data-slot` attributes the component stamps on each
412
+ * element. Injected once into `<head>` (id-guarded). This is what makes the
413
+ * inline component a polished drop-in instead of bare divs, while preserving
414
+ * the headless contract for anyone who wants to style it themselves.
415
+ */
416
+ const CHATBOT_INLINE_STYLE_ID = "biab-chatbot-inline-styles";
417
+ const CHATBOT_INLINE_CSS = `
418
+ :where([data-biab-chatbot]){font-size:14px;color:inherit;background:var(--biab-chatbot-bg,Canvas);border:1px solid color-mix(in srgb,currentColor 12%,transparent);border-radius:16px;}
419
+ :where([data-biab-chatbot] [data-slot="header"]){padding:12px 16px;border-bottom:1px solid color-mix(in srgb,currentColor 10%,transparent);}
420
+ :where([data-biab-chatbot] [data-slot="header-title"]){font-weight:600;}
421
+ :where([data-biab-chatbot] [data-slot="header-subtitle"]){font-size:12px;opacity:.6;margin-top:2px;}
422
+ :where([data-biab-chatbot] [data-slot="presence-strip"]){display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 16px;font-size:12px;border-bottom:1px solid color-mix(in srgb,currentColor 8%,transparent);opacity:.85;}
423
+ :where([data-biab-chatbot] [data-slot="message-list"]){padding:16px;gap:12px;}
424
+ :where([data-biab-chatbot] [data-slot="message-row"]){max-width:85%;width:fit-content;padding:8px 12px;border-radius:16px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere;box-shadow:0 1px 2px color-mix(in srgb,currentColor 8%,transparent);}
425
+ :where([data-biab-chatbot] [data-slot="message-row"][data-role="user"]){align-self:flex-end;background:var(--biab-chatbot-accent,#0d9488);color:#fff;border-bottom-right-radius:4px;}
426
+ :where([data-biab-chatbot] [data-slot="message-row"][data-role="assistant"]){align-self:flex-start;background:color-mix(in srgb,currentColor 8%,transparent);border-bottom-left-radius:4px;}
427
+ :where([data-biab-chatbot] [data-slot="message-pending"]){align-self:flex-start;width:fit-content;padding:8px 12px;background-image:linear-gradient(90deg,currentColor 35%,color-mix(in srgb,currentColor 25%,transparent) 50%,currentColor 65%);background-size:200% 100%;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;animation:biab-chatbot-shimmer 2s linear infinite;}
428
+ :where([data-biab-chatbot] [data-slot="error-text"]){align-self:center;font-size:12px;color:#dc2626;}
429
+ :where([data-biab-chatbot] [data-slot="input-bar"]){gap:8px;padding:12px;border-top:1px solid color-mix(in srgb,currentColor 10%,transparent);}
430
+ :where([data-biab-chatbot] [data-slot="input-field"]){padding:8px 12px;border-radius:10px;border:1px solid color-mix(in srgb,currentColor 15%,transparent);background:transparent;color:inherit;font:inherit;outline:none;}
431
+ :where([data-biab-chatbot] [data-slot="input-field"]:focus-visible){border-color:var(--biab-chatbot-accent,#0d9488);}
432
+ :where([data-biab-chatbot] [data-slot="send-button"]){padding:8px 14px;border-radius:10px;border:none;background:var(--biab-chatbot-accent,#0d9488);color:#fff;font-weight:500;cursor:pointer;}
433
+ :where([data-biab-chatbot] [data-slot="send-button"]:disabled){opacity:.5;cursor:default;}
434
+ :where([data-biab-chatbot] [data-slot="upload-button"]){display:inline-flex;align-items:center;justify-content:center;padding:0 6px;border:none;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer;opacity:.7;}
435
+ :where([data-biab-chatbot] [data-slot="upload-button"]:hover){opacity:1;}
436
+ :where([data-biab-chatbot] [data-slot="upload-button"]:disabled){opacity:.4;cursor:default;}
437
+ :where([data-biab-chatbot] [data-slot="message-file"]){align-self:flex-end;max-width:85%;padding:8px 12px;border-radius:16px;border-bottom-right-radius:4px;background:var(--biab-chatbot-accent,#0d9488);color:#fff;text-decoration:none;font-size:13px;word-break:break-all;}
438
+ :where([data-biab-chatbot] [data-slot="message-image"]){align-self:flex-end;max-width:85%;border-radius:12px;overflow:hidden;}
439
+ :where([data-biab-chatbot] [data-slot="jump-latest"]){position:absolute;left:50%;bottom:72px;transform:translateX(-50%);display:inline-flex;align-items:center;gap:4px;padding:6px 12px;border-radius:999px;border:1px solid color-mix(in srgb,currentColor 15%,transparent);background:var(--biab-chatbot-bg,Canvas);font-size:12px;font-weight:500;cursor:pointer;box-shadow:0 2px 10px color-mix(in srgb,currentColor 20%,transparent);}
440
+ :where([data-biab-chatbot] [data-slot="form-card"]){display:flex;flex-direction:column;gap:10px;align-self:stretch;padding:14px;border-radius:14px;border:1px solid color-mix(in srgb,currentColor 14%,transparent);background:color-mix(in srgb,currentColor 4%,transparent);}
441
+ :where([data-biab-chatbot] [data-slot="form-title"]){font-weight:600;}
442
+ :where([data-biab-chatbot] [data-slot="form-description"]){font-size:12px;opacity:.7;}
443
+ :where([data-biab-chatbot] [data-slot="form-field"]){gap:4px;}
444
+ :where([data-biab-chatbot] [data-slot="form-label"]){font-size:12px;font-weight:500;opacity:.85;}
445
+ :where([data-biab-chatbot] [data-slot="form-input"],[data-biab-chatbot] [data-slot="form-textarea"],[data-biab-chatbot] [data-slot="form-select"]){padding:8px 10px;border-radius:8px;border:1px solid color-mix(in srgb,currentColor 15%,transparent);background:transparent;color:inherit;font:inherit;outline:none;}
446
+ :where([data-biab-chatbot] [data-slot="form-input"]:focus-visible,[data-biab-chatbot] [data-slot="form-textarea"]:focus-visible,[data-biab-chatbot] [data-slot="form-select"]:focus-visible){border-color:var(--biab-chatbot-accent,#0d9488);}
447
+ :where([data-biab-chatbot] [data-slot="form-submit"]){padding:8px 14px;border-radius:8px;border:none;background:var(--biab-chatbot-accent,#0d9488);color:#fff;font-weight:500;cursor:pointer;}
448
+ :where([data-biab-chatbot] [data-slot="form-submit"]:disabled){opacity:.5;cursor:default;}
449
+ :where([data-biab-chatbot] [data-slot="form-error"]){font-size:12px;color:#dc2626;}
450
+ :where([data-biab-chatbot] [data-slot="form-receipt"]){font-size:13px;opacity:.8;}
451
+ @keyframes biab-chatbot-shimmer{from{background-position:100% 0;}to{background-position:0 0;}}
452
+ @media (prefers-reduced-motion:reduce){:where([data-biab-chatbot] [data-slot="message-pending"]){animation:none;-webkit-text-fill-color:currentColor;}}
453
+ @property --biab-chatbot-fade{syntax:"<length>";inherits:false;initial-value:0px;}
454
+ @keyframes biab-chatbot-fade-b{from{--biab-chatbot-fade:24px;}to{--biab-chatbot-fade:0px;}}
455
+ @supports (animation-timeline:scroll()){:where([data-biab-chatbot] [data-slot="message-list"]){-webkit-mask-image:linear-gradient(to bottom,#000 0,#000 calc(100% - var(--biab-chatbot-fade,0px)),transparent 100%);mask-image:linear-gradient(to bottom,#000 0,#000 calc(100% - var(--biab-chatbot-fade,0px)),transparent 100%);animation:biab-chatbot-fade-b 1ms linear both;animation-timeline:scroll(self y);animation-range:calc(100% - 96px) 100%;}}
456
+ `;
457
+ /**
458
+ * In-tree React chatbot component. Same backend wiring as the iframe
459
+ * `<Chatbot />` (chat → uiActions → form fetch → submit + CRM mirrors,
460
+ * plus availability polling) but rendered directly in the consumer's
461
+ * DOM tree so they can style every piece with their own CSS / Tailwind.
462
+ *
463
+ * For zero-customization drop-in use, prefer `<Chatbot />` (iframe).
464
+ * For fully custom UI, use the `useChatbot()` hook directly.
465
+ */
466
+ export function ChatbotInline({ apiKey, baseUrl, welcomeMessage, title = "Assistant", subtitle = null, classNames = {}, className, style, unstyled = false, availabilityPollMs = 30_000, renderMessage, renderPresenceStrip, onAssistantMessage, }) {
467
+ const { messages, send, isPending, error, latestUiActions, availability, refreshAvailability, getForm, submitForm, getConfig, uploadFile, } = useChatbot({ apiKey, baseUrl });
468
+ const [thread, setThread] = useState(() => welcomeMessage
469
+ ? [{ kind: "text", role: "assistant", content: welcomeMessage }]
470
+ : []);
471
+ const [draft, setDraft] = useState("");
472
+ const lastUserCount = useRef(0);
473
+ const lastAssistantCount = useRef(0);
474
+ const handledActionFingerprint = useRef("");
475
+ const scrollRef = useRef(null);
476
+ // Follow the live edge only while the reader is already at the bottom; the
477
+ // jump-to-latest control surfaces when they've scrolled up.
478
+ const followRef = useRef(true);
479
+ const lastThreadLen = useRef(0);
480
+ const [showJump, setShowJump] = useState(false);
481
+ // Visitor file uploads (entitlement-gated) + email capture.
482
+ const uploadSessionIdRef = useRef(null);
483
+ const fileUploadInputRef = useRef(null);
484
+ const [stagedFile, setStagedFile] = useState(null);
485
+ const [stagedPreview, setStagedPreview] = useState(null);
486
+ const [fileUploadsEnabled, setFileUploadsEnabled] = useState(false);
487
+ const [emailCaptureMode, setEmailCaptureMode] = useState("off");
488
+ const [capturedEmail, setCapturedEmail] = useState(null);
489
+ const [emailDraft, setEmailDraft] = useState("");
490
+ const [emailRequestDismissed, setEmailRequestDismissed] = useState(false);
491
+ const [isUploading, setIsUploading] = useState(false);
492
+ const [uploadError, setUploadError] = useState(null);
493
+ // Inject the default visual theme once (id-guarded). Skipped when `unstyled`.
494
+ // The rules are zero-specificity `:where(…)`, so any `classNames` a consumer
495
+ // passes still win — this only styles the otherwise-bare default drop-in.
496
+ useEffect(() => {
497
+ if (unstyled || typeof document === "undefined")
498
+ return;
499
+ if (document.getElementById(CHATBOT_INLINE_STYLE_ID))
500
+ return;
501
+ const styleEl = document.createElement("style");
502
+ styleEl.id = CHATBOT_INLINE_STYLE_ID;
503
+ styleEl.textContent = CHATBOT_INLINE_CSS;
504
+ document.head.appendChild(styleEl);
505
+ }, [unstyled]);
506
+ const scrollToEnd = useCallback((behavior = "auto") => {
507
+ const el = scrollRef.current;
508
+ if (!el)
509
+ return;
510
+ // Defer one frame so React has flushed the new node before we measure.
511
+ requestAnimationFrame(() => {
512
+ el.scrollTo({ top: el.scrollHeight, behavior });
513
+ });
514
+ }, []);
515
+ // Track whether the reader is at the live edge (24px slack); toggle the jump
516
+ // control accordingly.
517
+ const handleScroll = useCallback(() => {
518
+ const el = scrollRef.current;
519
+ if (!el)
520
+ return;
521
+ const bottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 24;
522
+ followRef.current = bottom;
523
+ setShowJump(!bottom && el.scrollHeight - el.clientHeight > 24);
524
+ }, []);
525
+ const jumpToLatest = useCallback(() => {
526
+ followRef.current = true;
527
+ setShowJump(false);
528
+ scrollToEnd("smooth");
529
+ }, [scrollToEnd]);
530
+ // Mirror useChatbot's `messages` into the thread so user input + assistant
531
+ // replies show up alongside any form cards we've stitched in.
532
+ useEffect(() => {
533
+ const prevUser = lastUserCount.current;
534
+ const prevAssistant = lastAssistantCount.current;
535
+ const userMsgs = messages.filter((m) => m.role === "user");
536
+ const assistantMsgs = messages.filter((m) => m.role === "assistant");
537
+ if (userMsgs.length > prevUser) {
538
+ const newOnes = userMsgs.slice(prevUser);
539
+ setThread((prev) => [
540
+ ...prev,
541
+ ...newOnes.map((m) => ({
542
+ kind: "text",
543
+ role: "user",
544
+ content: m.content,
545
+ })),
546
+ ]);
547
+ lastUserCount.current = userMsgs.length;
548
+ }
549
+ if (assistantMsgs.length > prevAssistant) {
550
+ const newOnes = assistantMsgs.slice(prevAssistant);
551
+ setThread((prev) => [
552
+ ...prev,
553
+ ...newOnes.map((m) => ({
554
+ kind: "text",
555
+ role: "assistant",
556
+ content: m.content,
557
+ })),
558
+ ]);
559
+ const lastReply = newOnes[newOnes.length - 1]?.content;
560
+ if (lastReply)
561
+ onAssistantMessage?.(lastReply);
562
+ lastAssistantCount.current = assistantMsgs.length;
563
+ }
564
+ }, [messages, onAssistantMessage]);
565
+ // Resolve `show_form` UI actions into inline form cards. Fingerprint by
566
+ // turn so we don't double-fetch the same recommendation.
567
+ useEffect(() => {
568
+ if (!latestUiActions.length)
569
+ return;
570
+ const fingerprint = latestUiActions
571
+ .map((a) => (a.kind === "show_form" ? a.formSlug : a.kind))
572
+ .join("|");
573
+ if (fingerprint === handledActionFingerprint.current)
574
+ return;
575
+ handledActionFingerprint.current = fingerprint;
576
+ let cancelled = false;
577
+ void (async () => {
578
+ for (const action of latestUiActions) {
579
+ if (action.kind !== "show_form")
580
+ continue;
581
+ const form = await getForm(action.formSlug);
582
+ if (cancelled || !form)
583
+ continue;
584
+ setThread((prev) => {
585
+ if (prev.some((item) => item.kind === "form" && item.form.slug === form.slug)) {
586
+ return prev;
587
+ }
588
+ return [...prev, { kind: "form", form, submitted: false }];
589
+ });
590
+ }
591
+ })();
592
+ return () => {
593
+ cancelled = true;
594
+ };
595
+ }, [latestUiActions, getForm]);
596
+ // New content: anchor a new USER turn near the top (with a 64px peek of the
597
+ // prior item) so the reply grows into view below it; otherwise follow the
598
+ // live edge only while the reader is already at the bottom. Replaces the old
599
+ // unconditional scroll-to-bottom that yanked readers who had scrolled up.
600
+ // biome-ignore lint/correctness/useExhaustiveDependencies: reads live DOM; thread/isPending are the triggers
601
+ useEffect(() => {
602
+ const el = scrollRef.current;
603
+ if (!el)
604
+ return;
605
+ const grew = thread.length > lastThreadLen.current;
606
+ const newest = thread[thread.length - 1];
607
+ const newestIsUser = grew && newest?.kind === "text" && newest.role === "user";
608
+ lastThreadLen.current = thread.length;
609
+ requestAnimationFrame(() => {
610
+ if (newestIsUser) {
611
+ const rows = el.querySelectorAll("[data-thread-row]");
612
+ const row = rows[rows.length - 1];
613
+ if (row) {
614
+ const target = row.getBoundingClientRect().top -
615
+ el.getBoundingClientRect().top +
616
+ el.scrollTop -
617
+ 64;
618
+ // Anchor only if the turn would otherwise overflow; else rest at end.
619
+ if (el.scrollHeight - target > el.clientHeight) {
620
+ el.scrollTo({ top: Math.max(0, target), behavior: "smooth" });
621
+ followRef.current = false;
622
+ }
623
+ else {
624
+ el.scrollTop = el.scrollHeight;
625
+ followRef.current = true;
626
+ }
627
+ }
628
+ }
629
+ else if (followRef.current) {
630
+ el.scrollTop = el.scrollHeight;
631
+ }
632
+ handleScroll();
633
+ });
634
+ }, [thread, isPending, handleScroll]);
635
+ useEffect(() => {
636
+ void refreshAvailability();
637
+ if (availabilityPollMs <= 0)
638
+ return;
639
+ const id = setInterval(() => {
640
+ void refreshAvailability();
641
+ }, availabilityPollMs);
642
+ return () => clearInterval(id);
643
+ }, [refreshAvailability, availabilityPollMs]);
644
+ // Read the org's upload + email-capture config once on mount.
645
+ useEffect(() => {
646
+ let cancelled = false;
647
+ void getConfig()
648
+ .then((cfg) => {
649
+ if (cancelled)
650
+ return;
651
+ setFileUploadsEnabled(cfg.fileUploadsEnabled);
652
+ setEmailCaptureMode(cfg.emailCaptureMode);
653
+ })
654
+ .catch(() => { });
655
+ return () => {
656
+ cancelled = true;
657
+ };
658
+ }, [getConfig]);
659
+ const handleSend = useCallback(async () => {
660
+ const trimmed = draft.trim();
661
+ if ((!trimmed && !stagedFile) || isPending || isUploading)
662
+ return;
663
+ // Upload a staged file first so it posts together with the text.
664
+ if (stagedFile) {
665
+ if (!uploadSessionIdRef.current) {
666
+ uploadSessionIdRef.current = crypto.randomUUID();
667
+ }
668
+ setUploadError(null);
669
+ setIsUploading(true);
670
+ try {
671
+ const fileRef = await uploadFile({
672
+ sessionId: uploadSessionIdRef.current,
673
+ file: stagedFile,
674
+ uploaderEmail: capturedEmail,
675
+ });
676
+ setThread((prev) => [...prev, { kind: "file", file: fileRef }]);
677
+ setStagedFile(null);
678
+ setStagedPreview(null);
679
+ }
680
+ catch (e) {
681
+ setUploadError(e instanceof Error ? e.message : "Upload failed.");
682
+ setIsUploading(false);
683
+ return;
684
+ }
685
+ setIsUploading(false);
686
+ }
687
+ if (trimmed) {
688
+ setDraft("");
689
+ await send(trimmed);
690
+ }
691
+ }, [
692
+ draft,
693
+ isPending,
694
+ isUploading,
695
+ stagedFile,
696
+ uploadFile,
697
+ capturedEmail,
698
+ send,
699
+ ]);
700
+ const handleFormSubmit = useCallback(async (slug, data) => {
701
+ const submitterEmail = (typeof data["email"] === "string" ? data["email"] : undefined) ??
702
+ (typeof data["contact_email"] === "string"
703
+ ? data["contact_email"]
704
+ : undefined);
705
+ const submitterName = (typeof data["name"] === "string" ? data["name"] : undefined) ??
706
+ (typeof data["full_name"] === "string" ? data["full_name"] : undefined);
707
+ const result = await submitForm(slug, {
708
+ data,
709
+ submitterEmail,
710
+ submitterName,
711
+ });
712
+ if (!result)
713
+ return false;
714
+ setThread((prev) => prev.map((item) => item.kind === "form" && item.form.slug === slug
715
+ ? { ...item, submitted: true }
716
+ : item));
717
+ return true;
718
+ }, [submitForm]);
719
+ const userMessageCount = thread.filter((t) => t.kind === "text" && t.role === "user").length;
720
+ const needsEmailGate = emailCaptureMode === "require" && !capturedEmail && userMessageCount === 0;
721
+ const showEmailRequest = emailCaptureMode === "request" &&
722
+ !capturedEmail &&
723
+ !emailRequestDismissed &&
724
+ userMessageCount === 0;
725
+ const submitEmailGate = useCallback(() => {
726
+ const email = emailDraft.trim();
727
+ if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
728
+ setCapturedEmail(email);
729
+ setEmailDraft("");
730
+ }
731
+ }, [emailDraft]);
732
+ const stageFile = useCallback((file) => {
733
+ // 7 MB cap (also enforced server-side on upload).
734
+ if (file.size > 7 * 1024 * 1024) {
735
+ setUploadError("That file is too large (max 7 MB).");
736
+ return;
737
+ }
738
+ setUploadError(null);
739
+ setStagedFile(file);
740
+ setStagedPreview(file.type.startsWith("image/") ? URL.createObjectURL(file) : null);
741
+ }, []);
742
+ const clearStaged = useCallback(() => {
743
+ setStagedFile(null);
744
+ setStagedPreview(null);
745
+ }, []);
746
+ useEffect(() => {
747
+ return () => {
748
+ if (stagedPreview)
749
+ URL.revokeObjectURL(stagedPreview);
750
+ };
751
+ }, [stagedPreview]);
752
+ return (_jsxs("div", { className: [classNames.root, className].filter(Boolean).join(" ") || undefined, "data-biab-chatbot": unstyled ? undefined : "", style: { ...inlineLayoutStyles.root, ...style }, children: [title !== null ? (_jsxs("div", { className: classNames.header, "data-slot": "header", children: [_jsx("div", { className: classNames.headerTitle, "data-slot": "header-title", children: title }), subtitle !== null && subtitle !== undefined ? (_jsx("div", { className: classNames.headerSubtitle, "data-slot": "header-subtitle", children: subtitle })) : null] })) : null, availability?.frontDeskEnabled
753
+ ? renderPresenceStrip
754
+ ? renderPresenceStrip(availability)
755
+ : defaultPresenceStrip({ availability, classNames })
756
+ : null, _jsxs("div", { className: classNames.messageList, "data-slot": "message-list", onScroll: handleScroll, ref: scrollRef, style: inlineLayoutStyles.messageList, children: [thread.map((item, idx) => {
757
+ if (renderMessage)
758
+ return renderMessage(item, idx);
759
+ if (item.kind === "form") {
760
+ return (_jsx(InlineFormCard, { classNames: classNames, form: item.form, onSubmit: (data) => handleFormSubmit(item.form.slug, data), submitted: item.submitted }, idx));
761
+ }
762
+ if (item.kind === "file") {
763
+ const isImage = item.file.type.startsWith("image/");
764
+ return (_jsx("a", { className: isImage ? undefined : classNames.messageUser, "data-role": "user", "data-slot": isImage ? "message-image" : "message-file", "data-thread-row": "", href: item.file.url, rel: "noreferrer", target: "_blank", children: isImage ? (_jsx("img", { alt: item.file.name, src: item.file.url, style: {
765
+ display: "block",
766
+ maxWidth: "100%",
767
+ maxHeight: 220,
768
+ borderRadius: 12,
769
+ } })) : (`📎 ${item.file.name}`) }, idx));
770
+ }
771
+ const bubbleClass = item.role === "user"
772
+ ? classNames.messageUser
773
+ : classNames.messageAssistant;
774
+ return (_jsx("div", { className: [classNames.messageRow, bubbleClass]
775
+ .filter(Boolean)
776
+ .join(" ") || undefined, "data-role": item.role, "data-slot": "message-row", "data-thread-row": "", children: item.content }, idx));
777
+ }), isPending ? (_jsx("div", { className: classNames.messagePending, "data-slot": "message-pending", children: "\u2026" })) : null, error ? (_jsx("div", { className: classNames.errorText, "data-slot": "error-text", children: error.message })) : null] }), showJump ? (_jsx("button", { "aria-label": "Jump to latest", className: classNames.jumpButton, "data-slot": "jump-latest", onClick: jumpToLatest, type: "button", children: "\u2193 Latest" })) : null, showEmailRequest ? (_jsxs("form", { "data-slot": "email-request", onSubmit: (e) => {
778
+ e.preventDefault();
779
+ submitEmailGate();
780
+ }, style: {
781
+ display: "flex",
782
+ gap: 8,
783
+ alignItems: "center",
784
+ padding: "8px 12px",
785
+ fontSize: 12,
786
+ }, children: [_jsx("input", { onChange: (e) => setEmailDraft(e.target.value), placeholder: "Your email (optional)", style: {
787
+ flex: 1,
788
+ background: "transparent",
789
+ border: "none",
790
+ outline: "none",
791
+ color: "inherit",
792
+ }, type: "email", value: emailDraft }), _jsx("button", { style: {
793
+ background: "none",
794
+ border: "none",
795
+ cursor: "pointer",
796
+ color: "inherit",
797
+ fontWeight: 500,
798
+ }, type: "submit", children: "Share" }), _jsx("button", { onClick: () => setEmailRequestDismissed(true), style: {
799
+ background: "none",
800
+ border: "none",
801
+ cursor: "pointer",
802
+ color: "inherit",
803
+ opacity: 0.7,
804
+ }, type: "button", children: "Skip" })] })) : null, needsEmailGate ? (_jsxs("form", { className: classNames.inputBar, "data-slot": "input-bar", onSubmit: (e) => {
805
+ e.preventDefault();
806
+ submitEmailGate();
807
+ }, style: inlineLayoutStyles.inputBar, children: [_jsx("input", { className: classNames.inputField, "data-slot": "input-field", onChange: (e) => setEmailDraft(e.target.value), placeholder: "Enter your email to start chatting", required: true, style: inlineLayoutStyles.inputField, type: "email", value: emailDraft }), _jsx("button", { className: classNames.sendButton, "data-slot": "send-button", type: "submit", children: "Start" })] })) : (_jsxs("form", { className: classNames.inputBar, "data-slot": "input-bar", onSubmit: (e) => {
808
+ e.preventDefault();
809
+ void handleSend();
810
+ }, style: inlineLayoutStyles.inputBar, children: [fileUploadsEnabled ? (_jsxs(_Fragment, { children: [_jsx("input", { onChange: (e) => {
811
+ const f = e.target.files?.[0];
812
+ e.target.value = "";
813
+ if (f)
814
+ stageFile(f);
815
+ }, ref: fileUploadInputRef, style: { display: "none" }, type: "file" }), _jsx("button", { "aria-label": "Attach a file", "data-slot": "upload-button", disabled: isPending, onClick: () => fileUploadInputRef.current?.click(), type: "button", children: "\uD83D\uDCCE" })] })) : null, _jsx("input", { className: classNames.inputField, "data-slot": "input-field", disabled: isPending, onChange: (e) => setDraft(e.target.value), placeholder: "Type a message\u2026", style: inlineLayoutStyles.inputField, value: draft }), _jsx("button", { className: classNames.sendButton, "data-slot": "send-button", disabled: isPending || isUploading || (!draft.trim() && !stagedFile), type: "submit", children: "Send" })] })), stagedFile ? (_jsxs("div", { "data-slot": "staged-attachment", style: {
816
+ display: "flex",
817
+ alignItems: "center",
818
+ gap: 8,
819
+ padding: "8px 12px",
820
+ borderTop: "1px solid color-mix(in srgb,currentColor 10%,transparent)",
821
+ }, children: [stagedPreview ? (_jsx("img", { alt: stagedFile.name, src: stagedPreview, style: {
822
+ width: 36,
823
+ height: 36,
824
+ borderRadius: 6,
825
+ objectFit: "cover",
826
+ flexShrink: 0,
827
+ } })) : (_jsx("span", { style: { fontSize: 18, flexShrink: 0 }, children: "\uD83D\uDCCE" })), _jsx("span", { style: {
828
+ flex: 1,
829
+ minWidth: 0,
830
+ overflow: "hidden",
831
+ textOverflow: "ellipsis",
832
+ whiteSpace: "nowrap",
833
+ fontSize: 13,
834
+ }, children: stagedFile.name }), _jsx("button", { "aria-label": "Remove file", onClick: clearStaged, style: {
835
+ border: "none",
836
+ background: "transparent",
837
+ color: "inherit",
838
+ cursor: "pointer",
839
+ opacity: 0.7,
840
+ fontSize: 16,
841
+ }, type: "button", children: "\u2715" })] })) : null, uploadError ? (_jsx("div", { className: classNames.errorText, "data-slot": "error-text", style: { padding: "0 12px 8px" }, children: uploadError })) : null] }));
842
+ }
843
+ function defaultPresenceStrip({ availability, classNames, }) {
844
+ const { staffAvailable, offline, estimateLabel, onlineHandlers } = availability;
845
+ const stateClass = offline
846
+ ? classNames.presenceStripOffline
847
+ : staffAvailable
848
+ ? classNames.presenceStripOnline
849
+ : classNames.presenceStripAway;
850
+ const label = offline
851
+ ? "Team is offline — leave a message and we'll follow up."
852
+ : staffAvailable
853
+ ? onlineHandlers.length === 0
854
+ ? "Team is online — we're here."
855
+ : onlineHandlers.length === 1
856
+ ? `${onlineHandlers[0]?.name} is here now.`
857
+ : `${onlineHandlers.length} teammates are here now.`
858
+ : "Team's away — we'll get back as soon as we can.";
859
+ return (_jsxs("div", { className: [classNames.presenceStrip, stateClass].filter(Boolean).join(" ") ||
860
+ undefined, "data-slot": "presence-strip", children: [_jsx("span", { className: classNames.presenceLabel, children: label }), estimateLabel ? (_jsx("span", { className: classNames.presenceEstimate, children: estimateLabel })) : null] }));
861
+ }
862
+ function InlineFormCard({ form, submitted, onSubmit, classNames, }) {
863
+ const [values, setValues] = useState({});
864
+ const [submitting, setSubmitting] = useState(false);
865
+ const [submitError, setSubmitError] = useState(null);
866
+ const inputFields = useMemo(() => {
867
+ const CONTAINER_FIELD_TYPES = new Set([
868
+ "input_group",
869
+ "condition_block",
870
+ "or_condition",
871
+ "flex_row",
872
+ "multi_populator",
873
+ ]);
874
+ const DISPLAY_ONLY_TYPES = new Set([
875
+ ...STATIC_FIELD_TYPES,
876
+ "body_text",
877
+ ]);
878
+ const out = [];
879
+ const walk = (list) => {
880
+ for (const f of list ?? []) {
881
+ const children = f.children;
882
+ if (CONTAINER_FIELD_TYPES.has(f.type) || Array.isArray(children)) {
883
+ walk(children);
884
+ }
885
+ else if (!DISPLAY_ONLY_TYPES.has(f.type)) {
886
+ out.push(f);
887
+ }
888
+ }
889
+ };
890
+ walk(form.fields);
891
+ return out;
892
+ }, [form.fields]);
893
+ if (submitted) {
894
+ return (_jsxs("div", { className: classNames.formReceipt, "data-slot": "form-receipt", children: ["Thanks \u2014 we got your ", form.name.toLowerCase(), "."] }));
895
+ }
896
+ return (_jsxs("form", { className: classNames.formCard, "data-slot": "form-card", onSubmit: async (e) => {
897
+ e.preventDefault();
898
+ setSubmitting(true);
899
+ setSubmitError(null);
900
+ const ok = await onSubmit(values);
901
+ if (!ok)
902
+ setSubmitError("Could not submit — please try again.");
903
+ setSubmitting(false);
904
+ }, children: [_jsxs("div", { children: [_jsx("div", { className: classNames.formTitle, "data-slot": "form-title", children: form.name }), form.description ? (_jsx("div", { className: classNames.formDescription, "data-slot": "form-description", children: form.description })) : null] }), inputFields.map((field) => {
905
+ const value = values[field.id] ?? "";
906
+ const setValue = (next) => setValues((prev) => ({ ...prev, [field.id]: next }));
907
+ if (field.type === "textarea") {
908
+ return (_jsxs("label", { className: classNames.formField, "data-slot": "form-field", style: inlineLayoutStyles.formField, children: [_jsxs("span", { className: classNames.formLabel, "data-slot": "form-label", children: [field.label, field.required ? " *" : ""] }), _jsx("textarea", { className: classNames.formTextarea, "data-slot": "form-textarea", onChange: (e) => setValue(e.target.value), placeholder: field.placeholder, required: field.required, rows: 3, value: typeof value === "string" ? value : "" })] }, field.id));
909
+ }
910
+ if (field.type === "select" && field.options?.length) {
911
+ return (_jsxs("label", { className: classNames.formField, "data-slot": "form-field", style: inlineLayoutStyles.formField, children: [_jsxs("span", { className: classNames.formLabel, "data-slot": "form-label", children: [field.label, field.required ? " *" : ""] }), _jsxs("select", { className: classNames.formSelect, "data-slot": "form-select", onChange: (e) => setValue(e.target.value), required: field.required, value: typeof value === "string" ? value : "", children: [_jsx("option", { value: "", children: "Choose\u2026" }), field.options.map((opt) => (_jsx("option", { value: opt, children: opt }, opt)))] })] }, field.id));
912
+ }
913
+ const inputType = field.type === "email"
914
+ ? "email"
915
+ : field.type === "phone"
916
+ ? "tel"
917
+ : field.type === "number"
918
+ ? "number"
919
+ : "text";
920
+ return (_jsxs("label", { className: classNames.formField, style: inlineLayoutStyles.formField, children: [_jsxs("span", { className: classNames.formLabel, "data-slot": "form-label", children: [field.label, field.required ? " *" : ""] }), _jsx("input", { className: classNames.formInput, "data-slot": "form-input", onChange: (e) => setValue(e.target.value), placeholder: field.placeholder, required: field.required, type: inputType, value: typeof value === "string" ? value : "" })] }, field.id));
921
+ }), submitError ? (_jsx("div", { className: classNames.formError, "data-slot": "form-error", children: submitError })) : null, _jsx("button", { className: classNames.formSubmit, "data-slot": "form-submit", disabled: submitting, type: "submit", children: submitting ? "Sending…" : "Send" })] }));
922
+ }
923
+ export function Blog({ apiKey, baseUrl, postSlug, className, style, title = "Blog", loadingFallback, errorFallback, onEvent, refreshLeadMs, }) {
924
+ const input = useMemo(() => (postSlug ? { postSlug } : undefined), [postSlug]);
925
+ const { state, refresh } = useEmbedSession((client) => client.blog.createSession(input), { apiKey, baseUrl, refreshLeadMs });
926
+ return (_jsx(EmbedIframe, { className: className, errorFallback: errorFallback, loadingFallback: loadingFallback, onEvent: onEvent, refresh: refresh, state: state, style: style, title: title }));
927
+ }
928
+ const DEFAULT_AUTH_BASE_PATH = "/api/biab-auth";
929
+ function buildAuthHref(subroute, props) {
930
+ const base = (props.basePath ?? DEFAULT_AUTH_BASE_PATH).replace(/\/$/, "");
931
+ const params = new URLSearchParams();
932
+ if (props.returnTo)
933
+ params.set("returnTo", props.returnTo);
934
+ if (props.loginHint)
935
+ params.set("loginHint", props.loginHint);
936
+ const qs = params.toString();
937
+ return `${base}/${subroute}${qs ? `?${qs}` : ""}`;
938
+ }
939
+ export function SignIn({ children, ...rest }) {
940
+ return (_jsx("a", { className: rest.className, href: buildAuthHref("sign-in", rest), style: rest.style, children: children ?? "Sign in" }));
941
+ }
942
+ export function SignUp({ children, ...rest }) {
943
+ return (_jsx("a", { className: rest.className, href: buildAuthHref("sign-up", rest), style: rest.style, children: children ?? "Create account" }));
944
+ }
945
+ export function SignOut({ children, basePath, className, style, }) {
946
+ return (_jsx("a", { className: className, href: buildAuthHref("sign-out", { basePath }), style: style, children: children ?? "Sign out" }));
947
+ }
948
+ /**
949
+ * Programmatic sign-in — navigates the browser through the auth handler's
950
+ * `/sign-in` route. Equivalent to clicking `<SignIn />`. Browser-only.
951
+ */
952
+ export function signIn(options = {}) {
953
+ if (typeof window === "undefined")
954
+ return;
955
+ window.location.assign(buildAuthHref("sign-in", options));
956
+ }
957
+ /**
958
+ * Programmatic sign-up — navigates through `/sign-up`. Equivalent to
959
+ * clicking `<SignUp />`. Browser-only.
960
+ */
961
+ export function signUp(options = {}) {
962
+ if (typeof window === "undefined")
963
+ return;
964
+ window.location.assign(buildAuthHref("sign-up", options));
965
+ }
966
+ /**
967
+ * Programmatic sign-out. Defaults to a redirect through the auth
968
+ * handler's `/sign-out` route (same as `<SignOut />`). Pass
969
+ * `{ mode: "fetch" }` for a fetch-based clear that resolves with
970
+ * `{ ok: true }` and does not navigate.
971
+ */
972
+ export async function signOut(options = {}) {
973
+ if (typeof window === "undefined")
974
+ return { ok: false };
975
+ const base = (options.basePath ?? DEFAULT_AUTH_BASE_PATH).replace(/\/$/, "");
976
+ if (options.mode === "fetch") {
977
+ try {
978
+ const res = await fetch(`${base}/sign-out`, {
979
+ method: "POST",
980
+ credentials: "include",
981
+ });
982
+ return { ok: res.ok };
983
+ }
984
+ catch {
985
+ return { ok: false };
986
+ }
987
+ }
988
+ window.location.assign(`${base}/sign-out`);
989
+ return { ok: true };
990
+ }
991
+ /**
992
+ * Programmatic "forgot password" — POSTs to the auth handler's
993
+ * `/password-reset` route, which the SDK forwards to BIAB. The server
994
+ * returns `{ ok: true }` regardless of whether an account matched
995
+ * (intentional anti-leak), and emails the platform-hosted reset link
996
+ * out-of-band when a match exists.
997
+ */
998
+ export async function requestPasswordReset(input) {
999
+ if (typeof window === "undefined")
1000
+ return { ok: false };
1001
+ const base = (input.basePath ?? DEFAULT_AUTH_BASE_PATH).replace(/\/$/, "");
1002
+ const body = { email: input.email };
1003
+ if (input.returnTo)
1004
+ body.returnTo = input.returnTo;
1005
+ try {
1006
+ const res = await fetch(`${base}/password-reset`, {
1007
+ method: "POST",
1008
+ credentials: "include",
1009
+ headers: { "Content-Type": "application/json" },
1010
+ body: JSON.stringify(body),
1011
+ });
1012
+ return { ok: res.ok };
1013
+ }
1014
+ catch {
1015
+ return { ok: false };
1016
+ }
1017
+ }
1018
+ /**
1019
+ * Reads the current tenant session by hitting the `/me` route the SDK's
1020
+ * auth-handler exposes on the tenant's own domain. No props needed because
1021
+ * the cookie + handler path are the source of truth.
1022
+ */
1023
+ export function useUser(options) {
1024
+ const basePath = (options?.basePath ?? DEFAULT_AUTH_BASE_PATH).replace(/\/$/, "");
1025
+ const [state, setState] = useState({
1026
+ status: "loading",
1027
+ user: null,
1028
+ });
1029
+ useEffect(() => {
1030
+ let cancelled = false;
1031
+ fetch(`${basePath}/me`, { credentials: "include" })
1032
+ .then((r) => (r.ok ? r.json() : null))
1033
+ .then((body) => {
1034
+ if (cancelled)
1035
+ return;
1036
+ if (body) {
1037
+ setState({ status: "signed-in", user: body });
1038
+ }
1039
+ else {
1040
+ setState({ status: "signed-out", user: null });
1041
+ }
1042
+ })
1043
+ .catch(() => {
1044
+ if (cancelled)
1045
+ return;
1046
+ setState({ status: "signed-out", user: null });
1047
+ });
1048
+ return () => {
1049
+ cancelled = true;
1050
+ };
1051
+ }, [basePath]);
1052
+ return state;
1053
+ }
1054
+ const FOLLOWER_STORE_PREFIX = "biab.followers.";
1055
+ function readLocalFollow(key) {
1056
+ if (typeof window === "undefined")
1057
+ return null;
1058
+ try {
1059
+ const raw = window.localStorage.getItem(key);
1060
+ if (!raw)
1061
+ return null;
1062
+ const parsed = JSON.parse(raw);
1063
+ return typeof parsed?.email === "string"
1064
+ ? { email: parsed.email, at: Number(parsed.at) || 0 }
1065
+ : null;
1066
+ }
1067
+ catch {
1068
+ return null;
1069
+ }
1070
+ }
1071
+ function writeLocalFollow(key, value) {
1072
+ if (typeof window === "undefined")
1073
+ return;
1074
+ try {
1075
+ window.localStorage.setItem(key, JSON.stringify(value));
1076
+ }
1077
+ catch {
1078
+ // storage disabled / quota — the local hint is best-effort.
1079
+ }
1080
+ }
1081
+ function clearLocalFollow(key) {
1082
+ if (typeof window === "undefined")
1083
+ return;
1084
+ try {
1085
+ window.localStorage.removeItem(key);
1086
+ }
1087
+ catch {
1088
+ // ignore
1089
+ }
1090
+ }
1091
+ /**
1092
+ * Visitor-facing followers/subscribe client, bound to a publishable token so it
1093
+ * works browser-side with no backend.
1094
+ *
1095
+ * const f = useFollowers({ token: pk, siteId });
1096
+ * if (!f.subscribedLocally) showForm(); // anonymous "already subscribed?" hint
1097
+ * await f.join({ email, name, source: "footer" }); // subscribe (+ remembers this browser)
1098
+ * const { follower } = await f.me(user.email); // authoritative status (logged-in)
1099
+ * await f.leave(email); // unsubscribe (+ forgets)
1100
+ *
1101
+ * Source of truth for "is this visitor subscribed":
1102
+ * - Logged-in → pass `user.email` to `me()` (authoritative, cross-device).
1103
+ * - Anonymous → `subscribedLocally` — a per-browser flag set on `join()`.
1104
+ * Email is the real key; this is just a privacy-friendly UX hint so an
1105
+ * anonymous visitor isn't shown the form forever. Clearing storage / a new
1106
+ * device re-shows it, and re-subscribing is idempotent (no dupes). IP is
1107
+ * deliberately NOT used (shared NAT hides the form from non-subscribers).
1108
+ *
1109
+ * `useUser()` covers the auth session; this covers follower status + subscribe,
1110
+ * which the session doesn't include. All actions are `followers:self`, so a
1111
+ * publishable token can run them.
1112
+ */
1113
+ export function useFollowers(options) {
1114
+ const { token, siteId, baseUrl } = options;
1115
+ const client = useMemo(() => createBiabClient({
1116
+ apiKey: token,
1117
+ siteId,
1118
+ baseUrl: baseUrl ?? DEFAULT_PACKAGE_BASE_URL,
1119
+ }), [token, siteId, baseUrl]);
1120
+ const storeKey = `${FOLLOWER_STORE_PREFIX}${siteId}`;
1121
+ const [local, setLocal] = useState(() => readLocalFollow(storeKey));
1122
+ useEffect(() => {
1123
+ setLocal(readLocalFollow(storeKey));
1124
+ }, [storeKey]);
1125
+ const join = useCallback(async (input) => {
1126
+ const res = await client.followers.join(input);
1127
+ const entry = { email: input.email, at: Date.now() };
1128
+ writeLocalFollow(storeKey, entry);
1129
+ setLocal(entry);
1130
+ return res;
1131
+ }, [client, storeKey]);
1132
+ const leave = useCallback(async (email) => {
1133
+ const res = await client.followers.leave({ email });
1134
+ clearLocalFollow(storeKey);
1135
+ setLocal(null);
1136
+ return res;
1137
+ }, [client, storeKey]);
1138
+ const me = useCallback((email) => client.followers.me({ email }), [client]);
1139
+ const edit = useCallback((email, patch) => client.followers.edit({ email, patch }), [client]);
1140
+ return useMemo(() => ({
1141
+ join,
1142
+ leave,
1143
+ me,
1144
+ edit,
1145
+ /** This browser already subscribed (instant, no network). Use to hide a
1146
+ * newsletter form for anonymous visitors. */
1147
+ subscribedLocally: local !== null,
1148
+ /** Email this browser subscribed with — pass to `me()` for an
1149
+ * authoritative re-check, or `null` if this browser hasn't subscribed. */
1150
+ localEmail: local?.email ?? null,
1151
+ }), [join, leave, me, edit, local]);
1152
+ }
1153
+ /**
1154
+ * Lazy-fetch a single marketing page by `pageKey`. The hook reads from the
1155
+ * SDK's shared cache when present (warmed by `useMarketingPagePreloader` or
1156
+ * a previous `useMarketingPage` render) and only hits the network when the
1157
+ * cache is empty — so navigating between pages on a many-page site only
1158
+ * fetches the page the visitor actually views.
1159
+ *
1160
+ * Pass `enabled: false` to gate the request on something async (e.g., wait
1161
+ * for the visitor to consent before loading the privacy page).
1162
+ */
1163
+ export function useMarketingPage(options) {
1164
+ const { apiKey, baseUrl, siteId, pageKey, enabled = true } = options;
1165
+ const client = useMemo(() => new BiabDevClient({ apiKey, baseUrl }), [apiKey, baseUrl]);
1166
+ const initial = client.site(siteId).marketingPages.getCached(pageKey);
1167
+ const [state, setState] = useState(initial
1168
+ ? { status: "ready", page: initial.page }
1169
+ : { status: "idle", page: null });
1170
+ const fetchOnce = useCallback(async (skipCache = false) => {
1171
+ if (!enabled)
1172
+ return;
1173
+ setState({ status: "loading", page: null });
1174
+ try {
1175
+ const response = await client
1176
+ .site(siteId)
1177
+ .marketingPages.get(pageKey, { skipCache });
1178
+ setState({ status: "ready", page: response.page });
1179
+ }
1180
+ catch (err) {
1181
+ setState({
1182
+ status: "error",
1183
+ page: null,
1184
+ error: err instanceof Error
1185
+ ? err
1186
+ : new Error("Marketing page load failed"),
1187
+ });
1188
+ }
1189
+ }, [client, siteId, pageKey, enabled]);
1190
+ useEffect(() => {
1191
+ if (!enabled)
1192
+ return;
1193
+ // Cache hit: render synchronously, no fetch.
1194
+ const hit = client.site(siteId).marketingPages.getCached(pageKey);
1195
+ if (hit) {
1196
+ setState({ status: "ready", page: hit.page });
1197
+ return;
1198
+ }
1199
+ void fetchOnce(false);
1200
+ }, [client, siteId, pageKey, enabled, fetchOnce]);
1201
+ const refresh = useCallback(() => fetchOnce(true), [fetchOnce]);
1202
+ return { ...state, refresh };
1203
+ }
1204
+ /**
1205
+ * Loads the payload-free directory of marketing pages for a site. Pair with
1206
+ * `useMarketingPage(siteId, pageKey)` to render the selected page lazily.
1207
+ */
1208
+ export function useMarketingPagesList(options) {
1209
+ const { apiKey, baseUrl, siteId, enabled = true } = options;
1210
+ const client = useMemo(() => new BiabDevClient({ apiKey, baseUrl }), [apiKey, baseUrl]);
1211
+ const [state, setState] = useState({
1212
+ status: "idle",
1213
+ pages: null,
1214
+ });
1215
+ const fetchOnce = useCallback(async () => {
1216
+ if (!enabled)
1217
+ return;
1218
+ setState({ status: "loading", pages: null });
1219
+ try {
1220
+ const response = await client
1221
+ .site(siteId)
1222
+ .marketingPages.list();
1223
+ setState({ status: "ready", pages: response.pages });
1224
+ }
1225
+ catch (err) {
1226
+ setState({
1227
+ status: "error",
1228
+ pages: null,
1229
+ error: err instanceof Error ? err : new Error("Marketing pages list failed"),
1230
+ });
1231
+ }
1232
+ }, [client, siteId, enabled]);
1233
+ useEffect(() => {
1234
+ void fetchOnce();
1235
+ }, [fetchOnce]);
1236
+ return { ...state, refresh: fetchOnce };
1237
+ }
1238
+ /**
1239
+ * Returns a stable `preload(pageKey)` function that warms the SDK cache for
1240
+ * the next page without rendering anything yet. Common pattern: wire it to
1241
+ * `<Link onPointerEnter>` / `onFocus` so the next page is ready by the time
1242
+ * the visitor clicks. Idempotent — repeat calls are free.
1243
+ */
1244
+ export function useMarketingPagePreloader(options) {
1245
+ const { apiKey, baseUrl, siteId } = options;
1246
+ const client = useMemo(() => new BiabDevClient({ apiKey, baseUrl }), [apiKey, baseUrl]);
1247
+ return useCallback(async (pageKey) => {
1248
+ try {
1249
+ return await client.site(siteId).marketingPages.prefetch(pageKey);
1250
+ }
1251
+ catch {
1252
+ return null;
1253
+ }
1254
+ }, [client, siteId]);
1255
+ }
1256
+ // ── Marketing-flow hooks (schema-driven, see ./react-marketing.tsx) ────────
1257
+ export { useMarketingBundlePreloader, useMarketingLocales, useMarketingPageBundle, useMarketingPageSeo, useMarketingSection, usePublishedMarketingSchema, } from "./react-marketing.js";
1258
+ // ── Customer notification destination verification (0.9.0) ─────────────────
1259
+ /**
1260
+ * Drop-in email-verification widget for the customer's notification
1261
+ * preferences page. Customer enters an email, we mint + send a
1262
+ * verification link, they click the link, the deep-link target
1263
+ * page consumes the token via {@link VerifyEmailConfirm}.
1264
+ */
1265
+ export function VerifyEmailFlow(props) {
1266
+ const { apiKey, baseUrl, sessionToken, currentEmail } = props;
1267
+ const client = useMemo(() => new BiabDevClient({ apiKey, baseUrl }), [apiKey, baseUrl]);
1268
+ const [email, setEmail] = useState(currentEmail ?? "");
1269
+ const [sent, setSent] = useState(false);
1270
+ const [busy, setBusy] = useState(false);
1271
+ const [error, setError] = useState(null);
1272
+ const handleSend = useCallback(async () => {
1273
+ setError(null);
1274
+ setBusy(true);
1275
+ try {
1276
+ await client.notifications
1277
+ .withSession(sessionToken)
1278
+ .startVerification({ kind: "email", destination: email });
1279
+ setSent(true);
1280
+ props.onSent?.(email);
1281
+ }
1282
+ catch (err) {
1283
+ setError(err instanceof Error ? err.message : "Couldn't send verification.");
1284
+ }
1285
+ finally {
1286
+ setBusy(false);
1287
+ }
1288
+ }, [client, email, props, sessionToken]);
1289
+ if (sent) {
1290
+ return (_jsx("div", { className: props.className, children: _jsxs("p", { children: [_jsx("strong", { children: "Check your inbox." }), " A verification link is on the way to ", email, ". It expires in 15 minutes."] }) }));
1291
+ }
1292
+ return (_jsxs("div", { className: props.className, children: [_jsxs("label", { style: { display: "block", marginBottom: 8 }, children: ["Email for shipping notifications", _jsx("input", { autoComplete: "email", onChange: (e) => setEmail(e.target.value), placeholder: "you@example.com", style: { display: "block", width: "100%", padding: 8 }, type: "email", value: email })] }), error ? (_jsx("p", { style: { color: "#dc2626", margin: "4px 0" }, children: error })) : null, _jsx("button", { disabled: busy || !email.includes("@"), onClick: handleSend, type: "button", children: busy ? "Sending…" : "Send verification link" })] }));
1293
+ }
1294
+ /**
1295
+ * Deep-link target for an email verification flow. Reads the `token`
1296
+ * prop (typically pulled from the URL query string), confirms it
1297
+ * server-side, and renders success / failure.
1298
+ */
1299
+ export function VerifyEmailConfirm(props) {
1300
+ const { apiKey, baseUrl, sessionToken, token, onVerified } = props;
1301
+ const client = useMemo(() => new BiabDevClient({ apiKey, baseUrl }), [apiKey, baseUrl]);
1302
+ const [state, setState] = useState({ kind: "idle" });
1303
+ useEffect(() => {
1304
+ let cancelled = false;
1305
+ (async () => {
1306
+ try {
1307
+ const r = await client.notifications
1308
+ .withSession(sessionToken)
1309
+ .confirmVerification({ token });
1310
+ if (cancelled)
1311
+ return;
1312
+ setState({ kind: "ok", destination: r.destination });
1313
+ onVerified?.(r.destination);
1314
+ }
1315
+ catch (err) {
1316
+ if (cancelled)
1317
+ return;
1318
+ setState({
1319
+ kind: "err",
1320
+ message: err instanceof Error ? err.message : "Verification failed.",
1321
+ });
1322
+ }
1323
+ })();
1324
+ return () => {
1325
+ cancelled = true;
1326
+ };
1327
+ }, [client, onVerified, sessionToken, token]);
1328
+ if (state.kind === "idle")
1329
+ return _jsx("div", { className: props.className, children: "Verifying\u2026" });
1330
+ if (state.kind === "err") {
1331
+ return (_jsxs("div", { className: props.className, children: [_jsxs("p", { style: { color: "#dc2626" }, children: [_jsx("strong", { children: "Couldn't verify." }), " ", state.message] }), _jsx("p", { children: "The link may have expired. Request a new one from your notification preferences page." })] }));
1332
+ }
1333
+ return (_jsx("div", { className: props.className, children: _jsxs("p", { children: [_jsx("strong", { children: "Verified \u2713" }), " Notifications will now send to", " ", state.destination, "."] }) }));
1334
+ }
1335
+ /**
1336
+ * Inline SMS-OTP verification flow. Two phases, no round-trip
1337
+ * deep link. 6-digit OTP, 5-minute TTL.
1338
+ */
1339
+ export function VerifyPhoneFlow(props) {
1340
+ const { apiKey, baseUrl, sessionToken, currentPhone, onVerified } = props;
1341
+ const client = useMemo(() => new BiabDevClient({ apiKey, baseUrl }), [apiKey, baseUrl]);
1342
+ const [phone, setPhone] = useState(currentPhone ?? "");
1343
+ const [phase, setPhase] = useState("enter");
1344
+ const [otp, setOtp] = useState("");
1345
+ const [busy, setBusy] = useState(false);
1346
+ const [error, setError] = useState(null);
1347
+ const handleSend = useCallback(async () => {
1348
+ setError(null);
1349
+ setBusy(true);
1350
+ try {
1351
+ await client.notifications
1352
+ .withSession(sessionToken)
1353
+ .startVerification({ kind: "phone", destination: phone });
1354
+ setPhase("otp");
1355
+ }
1356
+ catch (err) {
1357
+ setError(err instanceof Error ? err.message : "Couldn't send code.");
1358
+ }
1359
+ finally {
1360
+ setBusy(false);
1361
+ }
1362
+ }, [client, phone, sessionToken]);
1363
+ const handleConfirm = useCallback(async () => {
1364
+ setError(null);
1365
+ setBusy(true);
1366
+ try {
1367
+ const r = await client.notifications
1368
+ .withSession(sessionToken)
1369
+ .confirmVerification({ token: otp });
1370
+ setPhase("done");
1371
+ onVerified?.(r.destination);
1372
+ }
1373
+ catch (err) {
1374
+ setError(err instanceof Error ? err.message : "Code didn't match.");
1375
+ }
1376
+ finally {
1377
+ setBusy(false);
1378
+ }
1379
+ }, [client, onVerified, otp, sessionToken]);
1380
+ if (phase === "done") {
1381
+ return (_jsx("div", { className: props.className, children: _jsxs("p", { children: [_jsx("strong", { children: "Verified \u2713" }), " SMS notifications will now go to ", phone, "."] }) }));
1382
+ }
1383
+ if (phase === "otp") {
1384
+ return (_jsxs("div", { className: props.className, children: [_jsxs("p", { children: ["We sent a 6-digit code to ", phone, ". Enter it below."] }), _jsx("input", { autoComplete: "one-time-code", inputMode: "numeric", maxLength: 6, onChange: (e) => setOtp(e.target.value.replace(/\D/g, "")), placeholder: "123456", style: {
1385
+ display: "block",
1386
+ width: "100%",
1387
+ padding: 8,
1388
+ letterSpacing: 4,
1389
+ fontSize: 18,
1390
+ }, type: "text", value: otp }), error ? (_jsx("p", { style: { color: "#dc2626", margin: "4px 0" }, children: error })) : null, _jsx("button", { disabled: busy || otp.length !== 6, onClick: handleConfirm, type: "button", children: busy ? "Verifying…" : "Confirm" })] }));
1391
+ }
1392
+ return (_jsxs("div", { className: props.className, children: [_jsxs("label", { style: { display: "block", marginBottom: 8 }, children: ["Phone number for SMS notifications", _jsx("input", { autoComplete: "tel", onChange: (e) => setPhone(e.target.value), placeholder: "+1 415 555 0123", style: { display: "block", width: "100%", padding: 8 }, type: "tel", value: phone })] }), error ? (_jsx("p", { style: { color: "#dc2626", margin: "4px 0" }, children: error })) : null, _jsx("button", { disabled: busy || phone.length < 7, onClick: handleSend, type: "button", children: busy ? "Sending…" : "Send code" })] }));
1393
+ }
1394
+ // ─── Element rendering ─────────────────────────────────────────────────────
1395
+ // Search thresholds and icon-URL resolution live in `forms-core/elements` so
1396
+ // every binding agrees; only the view layer is per-framework.
1397
+ /**
1398
+ * `icon_select` — pick an icon by name from a searchable grid.
1399
+ *
1400
+ * The name list is fetched lazily from the platform on first render rather than
1401
+ * bundled: it's ~1,600 entries that change only when Lucide does. An author who
1402
+ * curated `allowedIcons` skips the fetch entirely.
1403
+ */
1404
+ function BiabIconSelect({ field, value, set, }) {
1405
+ const cfg = field
1406
+ .iconConfig ?? {};
1407
+ const curated = cfg.allowedIcons ?? [];
1408
+ const [all, setAll] = useState(curated);
1409
+ const [query, setQuery] = useState("");
1410
+ const selected = typeof value === "string" ? value : "";
1411
+ useEffect(() => {
1412
+ if (curated.length > 0)
1413
+ return; // nothing to fetch
1414
+ let cancelled = false;
1415
+ // Resolves to [] on failure rather than throwing — an empty grid is
1416
+ // recoverable, a crashed form isn't.
1417
+ void fetchIconNames().then((names) => {
1418
+ if (!cancelled)
1419
+ setAll(names);
1420
+ });
1421
+ return () => {
1422
+ cancelled = true;
1423
+ };
1424
+ }, [curated.length]);
1425
+ const results = useMemo(() => filterIconNames(all, query), [all, query]);
1426
+ return (_jsxs("div", { className: "biab-iconselect", children: [_jsx("input", { className: "biab-input biab-iconselect__search", onChange: (e) => setQuery(e.target.value), placeholder: field.placeholder || "Search icons…", type: "text", value: query }), selected && (_jsxs("span", { className: "biab-iconselect__current", children: [_jsx("img", { alt: selected, height: 16, src: iconUrl(selected), width: 16 }), _jsx("button", { className: "biab-iconselect__clear", onClick: () => set(""), type: "button", children: "\u00D7" })] })), _jsx("div", { className: "biab-iconselect__grid", children: results.map((name) => (_jsx("button", { "aria-label": name, "aria-pressed": selected === name, className: `biab-iconselect__item${selected === name ? "is-selected" : ""}`, onClick: () => set(name), title: name, type: "button", children: _jsx("img", { alt: "", height: 16, src: iconUrl(name), width: 16 }) }, name))) })] }));
1427
+ }
1428
+ /**
1429
+ * `image_input` — one image, from an upload OR a pasted URL.
1430
+ *
1431
+ * The submitted value is a plain URL string either way. An upload is held as a
1432
+ * pending file and flushed by the controller at submit, exactly like
1433
+ * `file_upload`, then replaced by its stored URL.
1434
+ */
1435
+ function BiabImageInput({ field, value, set, }) {
1436
+ const cfg = field.imageInputConfig ?? {};
1437
+ const sources = cfg.sources ?? "both";
1438
+ const [mode, setMode] = useState(sources === "url" ? "url" : "upload");
1439
+ const pending = isPendingFileValue(value) ? value : null;
1440
+ const url = typeof value === "string" ? value : "";
1441
+ return (_jsxs("div", { className: "biab-imageinput", children: [sources === "both" && (_jsx("div", { className: "biab-imageinput__modes", children: ["upload", "url"].map((m) => (_jsx("button", { "aria-pressed": mode === m, className: `biab-imageinput__mode${mode === m ? "is-active" : ""}`, onClick: () => setMode(m), type: "button", children: m === "upload" ? "Upload" : "Paste URL" }, m))) })), mode === "upload" && sources !== "url" ? (_jsx("input", { accept: (cfg.allowedTypes ?? ["image/*"]).join(","), className: "biab-input biab-imageinput__file", onChange: (e) => {
1442
+ const file = e.target.files?.[0];
1443
+ if (!file)
1444
+ return;
1445
+ const maxBytes = (cfg.maxSize ?? 5) * 1024 * 1024;
1446
+ if (file.size > maxBytes)
1447
+ return;
1448
+ set({
1449
+ pending: true,
1450
+ file,
1451
+ name: file.name,
1452
+ size: file.size,
1453
+ });
1454
+ }, type: "file" })) : (_jsx("input", { className: "biab-input biab-imageinput__url", onChange: (e) => set(e.target.value), placeholder: field.placeholder || "https://example.com/image.png", type: "url", value: url })), cfg.showPreview !== false && (pending || url) ? (_jsx("img", { alt: "", className: "biab-imageinput__preview", src: pending ? "" : url })) : null, pending && (_jsxs("span", { className: "biab-imageinput__pending", children: [pending.name, " \u2014 uploads on submit."] }))] }));
1455
+ }
1456
+ // A `BiabFormProvider` lets a consumer supply the client once at the root and
1457
+ // drop `<BiabForm slug="…" />` anywhere below without threading the client prop.
1458
+ const BiabFormClientContext = createContext(null);
1459
+ /** Production package-API base URL — the default target for a publishable token
1460
+ * when no `baseUrl` is supplied. Override via the `baseUrl` prop for staging or
1461
+ * a custom host. Use the **apex** host: `www.biab.app` 307-redirects to the
1462
+ * apex, and a CORS preflight does not follow redirects (the browser fails the
1463
+ * request), so a browser publishable-token client must target the apex. */
1464
+ export const DEFAULT_PACKAGE_BASE_URL = "https://biab.app/api/package/v1";
1465
+ /** Provide a BIAB forms client to every nested `<BiabForm>`. Pass a `client`, or
1466
+ * a publishable `token` + `siteId` (+ optional `baseUrl`) to build a browser
1467
+ * client. */
1468
+ export function BiabFormProvider({ client, token, siteId, baseUrl, children, }) {
1469
+ const resolved = useMemo(() => client ??
1470
+ (token && siteId
1471
+ ? createBiabClient({
1472
+ apiKey: token,
1473
+ siteId,
1474
+ baseUrl: baseUrl ?? DEFAULT_PACKAGE_BASE_URL,
1475
+ })
1476
+ : null), [client, token, siteId, baseUrl]);
1477
+ return (_jsx(BiabFormClientContext.Provider, { value: resolved, children: children }));
1478
+ }
1479
+ /** Read the client from a surrounding `<BiabFormProvider>`, if any. */
1480
+ export function useBiabFormsClient() {
1481
+ return useContext(BiabFormClientContext);
1482
+ }
1483
+ const FetchScheduleSlotsContext = createContext(undefined);
1484
+ /** Autofill (`autocomplete`) config for the field tree, provided by `<BiabForm>`.
1485
+ * Default `{}` → inference on, no overrides. */
1486
+ const AutocompleteContext = createContext({});
1487
+ /** Auto-schedule (instant booking) picker for `<BiabForm>`: fetches the assigned
1488
+ * staff's open slots and lets the visitor pick one. Value `{ slotStartAt,
1489
+ * timezone }`; the server books the meeting on submit. Dependency-light (plain
1490
+ * elements + `biab-field__schedule-*` classes). */
1491
+ function BiabScheduleAuto({ field, value, onChange, }) {
1492
+ const fetchSlots = useContext(FetchScheduleSlotsContext);
1493
+ const v = value && typeof value === "object" ? value : {};
1494
+ const tz = typeof Intl !== "undefined"
1495
+ ? (Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC")
1496
+ : "UTC";
1497
+ const [slots, setSlots] = useState([]);
1498
+ const [phase, setPhase] = useState("idle");
1499
+ useEffect(() => {
1500
+ if (!fetchSlots)
1501
+ return;
1502
+ let cancelled = false;
1503
+ setPhase("loading");
1504
+ const from = new Date();
1505
+ const to = new Date(from.getTime() + 21 * 24 * 60 * 60 * 1000);
1506
+ fetchSlots({ fieldId: field.id, from, to })
1507
+ .then((res) => {
1508
+ if (cancelled)
1509
+ return;
1510
+ setSlots(res);
1511
+ setPhase("loaded");
1512
+ })
1513
+ .catch(() => {
1514
+ if (!cancelled)
1515
+ setPhase("error");
1516
+ });
1517
+ return () => {
1518
+ cancelled = true;
1519
+ };
1520
+ }, [fetchSlots, field.id]);
1521
+ if (!fetchSlots) {
1522
+ return (_jsx("p", { className: "biab-field__schedule-hint", children: "Booking isn't available here." }));
1523
+ }
1524
+ if (phase === "loading" || phase === "idle") {
1525
+ return (_jsx("p", { className: "biab-field__schedule-hint", children: "Loading available times\u2026" }));
1526
+ }
1527
+ if (phase === "error") {
1528
+ return (_jsx("p", { className: "biab-field__schedule-hint", children: "Couldn't load times right now." }));
1529
+ }
1530
+ if (slots.length === 0) {
1531
+ return (_jsx("p", { className: "biab-field__schedule-hint", children: "No open times in the next few weeks." }));
1532
+ }
1533
+ const byDay = new Map();
1534
+ for (const s of slots) {
1535
+ const d = new Date(s.startAt);
1536
+ const dayKey = d.toLocaleDateString(undefined, {
1537
+ weekday: "short",
1538
+ month: "short",
1539
+ day: "numeric",
1540
+ });
1541
+ const label = d.toLocaleTimeString(undefined, {
1542
+ hour: "numeric",
1543
+ minute: "2-digit",
1544
+ });
1545
+ const arr = byDay.get(dayKey) ?? [];
1546
+ arr.push({ startAt: s.startAt, label });
1547
+ byDay.set(dayKey, arr);
1548
+ }
1549
+ return (_jsx("div", { className: "biab-field__schedule-auto", children: [...byDay.entries()].map(([day, times]) => (_jsxs("div", { className: "biab-field__schedule-day", children: [_jsx("p", { className: "biab-field__schedule-day-label", children: day }), _jsx("div", { className: "biab-field__schedule-times", children: times.map((t) => (_jsx("button", { className: "biab-field__schedule-slot", "data-selected": v.slotStartAt === t.startAt ? "true" : undefined, onClick: () => onChange({ slotStartAt: t.startAt, timezone: tz }), type: "button", children: t.label }, t.startAt))) })] }, day))) }));
1550
+ }
1551
+ /**
1552
+ * Subscribe a component to a `FormController` via React's official external-store
1553
+ * primitive. `getSnapshot` returns the controller's cached snapshot object, which
1554
+ * the store only re-creates on mutation, so referential equality gives React a
1555
+ * correct bail-out without extra memoization. SSR uses the same snapshot.
1556
+ */
1557
+ function useFormSnapshot(controller) {
1558
+ const subscribe = useCallback((onStoreChange) => controller.subscribe(() => onStoreChange()), [controller]);
1559
+ const getSnapshot = useCallback(() => controller.snapshot(), [controller]);
1560
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1561
+ }
1562
+ // ── biab-* class helpers (mirrors FormPreview's `fieldCssKey`) ───────────────
1563
+ function cx(...parts) {
1564
+ return parts.filter(Boolean).join(" ");
1565
+ }
1566
+ /** camelCase a label into a stable, override-friendly CSS key. */
1567
+ function toOutputKeyLite(label) {
1568
+ return label
1569
+ .trim()
1570
+ .toLowerCase()
1571
+ .replace(/[^a-z0-9]+(.)?/g, (_m, c) => c ? c.toUpperCase() : "");
1572
+ }
1573
+ function fieldCssKey(field) {
1574
+ const explicit = typeof field.outputKey === "string" ? field.outputKey.trim() : "";
1575
+ if (explicit)
1576
+ return explicit;
1577
+ const lbl = typeof field.label === "string" ? field.label.trim() : "";
1578
+ return (lbl && toOutputKeyLite(lbl)) || field.id;
1579
+ }
1580
+ /** Format a phone's local part as the user types and cap the digit count.
1581
+ * National (US/CA) → `(555) 123-4567` (10-digit cap). International → loose
1582
+ * 3-digit grouping preserving a leading `+` (15-digit E.164 cap). Idempotent,
1583
+ * so it's safe as a controlled value. The submitted value carries the
1584
+ * formatting; server + SDK validation strip non-digits, so it stays valid. */
1585
+ function formatPhoneLocal(input, intl) {
1586
+ if (intl) {
1587
+ const plus = input.trimStart().startsWith("+") ? "+" : "";
1588
+ const digits = input.replace(/\D/g, "").slice(0, 15);
1589
+ return `${plus}${digits.replace(/(\d{3})(?=\d)/g, "$1 ")}`.trim();
1590
+ }
1591
+ // National (US/CA) — mirrors the canonical formatPhoneNumber: bare digits up to
1592
+ // 3, `(555) 123` up to 6, then `(555) 123-4567` (display caps at 10 digits).
1593
+ const cleaned = input.replace(/\D/g, "");
1594
+ if (cleaned.length <= 3)
1595
+ return cleaned;
1596
+ if (cleaned.length <= 6)
1597
+ return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3)}`;
1598
+ return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6, 10)}`;
1599
+ }
1600
+ /** Table "check" column cell → ✓ / – / plain text. Mirrors the app's copy in
1601
+ * `src/shared/forms/table-cell.ts`. */
1602
+ function tableCheckState(cell) {
1603
+ const v = cell.trim().toLowerCase();
1604
+ if (["yes", "y", "true", "1", "x", "✓", "✔", "check"].includes(v))
1605
+ return "yes";
1606
+ if (["no", "n", "false", "0", "-", "–", "—", ""].includes(v))
1607
+ return "no";
1608
+ return "text";
1609
+ }
1610
+ /**
1611
+ * Guard a user-supplied href before rendering it as a link: block dangerous
1612
+ * schemes (javascript:/data:/vbscript:/file:) that would be an XSS vector, while
1613
+ * allowing http(s) and relative paths. Returns undefined when unsafe so the
1614
+ * caller renders no link. (The SDK renders plain `<a>` — framework-agnostic, no
1615
+ * next/link — so this is the only hardening needed.)
1616
+ */
1617
+ function safeHttpHref(raw) {
1618
+ if (!raw)
1619
+ return undefined;
1620
+ const s = raw.trim();
1621
+ if (!s)
1622
+ return undefined;
1623
+ const collapsed = s.toLowerCase().replace(/\s/g, "");
1624
+ if (collapsed.startsWith("javascript:") ||
1625
+ collapsed.startsWith("data:") ||
1626
+ collapsed.startsWith("vbscript:") ||
1627
+ collapsed.startsWith("file:")) {
1628
+ return undefined;
1629
+ }
1630
+ return s;
1631
+ }
1632
+ // ── Self-contained date-range picker (no external calendar dep) ─────────────
1633
+ // A popover trigger + a month grid with click-to-select range. Stored value is
1634
+ // `{ start, end }` as `yyyy-MM-dd` strings (identical to the app's contract).
1635
+ // Styled via biab-* classes so consumers can theme it.
1636
+ function parseIsoDate(s) {
1637
+ if (!s)
1638
+ return null;
1639
+ const d = new Date(`${s}T00:00:00`);
1640
+ return Number.isNaN(d.getTime()) ? null : d;
1641
+ }
1642
+ function toIsoDate(d) {
1643
+ const y = d.getFullYear();
1644
+ const m = String(d.getMonth() + 1).padStart(2, "0");
1645
+ const day = String(d.getDate()).padStart(2, "0");
1646
+ return `${y}-${m}-${day}`;
1647
+ }
1648
+ const BIAB_MONTHS = [
1649
+ "January",
1650
+ "February",
1651
+ "March",
1652
+ "April",
1653
+ "May",
1654
+ "June",
1655
+ "July",
1656
+ "August",
1657
+ "September",
1658
+ "October",
1659
+ "November",
1660
+ "December",
1661
+ ];
1662
+ const BIAB_WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
1663
+ function BiabDateRange({ value, setValue, }) {
1664
+ const v = (value && typeof value === "object" ? value : {});
1665
+ const start = parseIsoDate(v.start);
1666
+ const end = parseIsoDate(v.end);
1667
+ const [open, setOpen] = useState(false);
1668
+ const [view, setView] = useState(() => {
1669
+ const base = start ?? new Date();
1670
+ return { y: base.getFullYear(), m: base.getMonth() };
1671
+ });
1672
+ const ref = useRef(null);
1673
+ useEffect(() => {
1674
+ if (!open)
1675
+ return;
1676
+ const onDoc = (e) => {
1677
+ if (ref.current && !ref.current.contains(e.target)) {
1678
+ setOpen(false);
1679
+ }
1680
+ };
1681
+ document.addEventListener("mousedown", onDoc);
1682
+ return () => document.removeEventListener("mousedown", onDoc);
1683
+ }, [open]);
1684
+ const pick = (day) => {
1685
+ const iso = toIsoDate(day);
1686
+ // First click (or restart after a complete range) sets the start.
1687
+ if (!start || (start && end)) {
1688
+ setValue({ start: iso, end: "" });
1689
+ return;
1690
+ }
1691
+ // Second click: order the two so start <= end (`start` is non-null here).
1692
+ if (day < start)
1693
+ setValue({ start: iso, end: toIsoDate(start) });
1694
+ else
1695
+ setValue({ start: toIsoDate(start), end: iso });
1696
+ };
1697
+ const firstDow = new Date(view.y, view.m, 1).getDay();
1698
+ const daysInMonth = new Date(view.y, view.m + 1, 0).getDate();
1699
+ const cells = [];
1700
+ for (let i = 0; i < firstDow; i++)
1701
+ cells.push(null);
1702
+ for (let d = 1; d <= daysInMonth; d++)
1703
+ cells.push(new Date(view.y, view.m, d));
1704
+ const sameDay = (a, b) => toIsoDate(a) === toIsoDate(b);
1705
+ const today = new Date();
1706
+ // COSS/shadcn range-state flags (drive the data-* attrs the CSS targets).
1707
+ const dayState = (d) => ({
1708
+ start: !!start && sameDay(d, start),
1709
+ end: !!end && sameDay(d, end),
1710
+ middle: !!start && !!end && d > start && d < end,
1711
+ single: !!start && !end && sameDay(d, start),
1712
+ today: sameDay(d, today),
1713
+ });
1714
+ const label = start
1715
+ ? end
1716
+ ? `${v.start} → ${v.end}`
1717
+ : `${v.start} → …`
1718
+ : "Pick a date range";
1719
+ // Positioning is the only inline style (so the popover never overlaps content
1720
+ // even before the CSS loads); everything visual lives in `biab-forms.css`,
1721
+ // which mirrors the COSS/shadcn calendar (import it from the SDK).
1722
+ return (_jsxs("div", { className: "biab-daterange", ref: ref, style: { position: "relative" }, children: [_jsx("button", { className: "biab-input biab-field__input biab-daterange__trigger", onClick: () => setOpen((o) => !o), type: "button", children: label }), open && (_jsxs("div", { className: "biab-daterange__popover", style: { position: "absolute", zIndex: 50 }, children: [_jsxs("div", { className: "biab-daterange__nav", children: [_jsx("button", { "aria-label": "Previous month", className: "biab-daterange__navbtn", onClick: () => setView((s) => s.m === 0 ? { y: s.y - 1, m: 11 } : { y: s.y, m: s.m - 1 }), type: "button", children: "\u2039" }), _jsxs("span", { className: "biab-daterange__title", children: [BIAB_MONTHS[view.m], " ", view.y] }), _jsx("button", { "aria-label": "Next month", className: "biab-daterange__navbtn", onClick: () => setView((s) => s.m === 11 ? { y: s.y + 1, m: 0 } : { y: s.y, m: s.m + 1 }), type: "button", children: "\u203A" })] }), _jsxs("div", { className: "biab-daterange__grid", children: [BIAB_WEEKDAYS.map((w) => (_jsx("span", { className: "biab-daterange__dow", children: w }, w))), cells.map((d, i) => {
1723
+ if (!d) {
1724
+ return (_jsx("span", { className: "biab-daterange__blank" }, `b-${i}`));
1725
+ }
1726
+ const st = dayState(d);
1727
+ return (_jsx("button", { className: "biab-daterange__day", "data-range-end": st.end || undefined, "data-range-middle": st.middle || undefined, "data-range-start": st.start || undefined, "data-selected-single": st.single || undefined, "data-today": st.today || undefined, onClick: () => pick(d), type: "button", children: d.getDate() }, toIsoDate(d)));
1728
+ })] })] }))] }));
1729
+ }
1730
+ // ── Per-type input renderer (light DOM, biab-* classes, no shadcn) ──────────
1731
+ function BiabFieldInput({ field, value, setValue, }) {
1732
+ const set = (v) => setValue(field.id, v);
1733
+ const ph = field.placeholder || "";
1734
+ // Resolved HTML autofill token for this field (or undefined to omit the
1735
+ // attribute). Honors the global `<BiabForm autoComplete>` switch and per-field
1736
+ // overrides; see forms-core/autocomplete.ts.
1737
+ const acConfig = useContext(AutocompleteContext);
1738
+ const autoComplete = resolveFieldAutocomplete(field, acConfig);
1739
+ switch (field.type) {
1740
+ case "title":
1741
+ return _jsx("h2", { className: "biab-field__title", children: field.label || "Untitled" });
1742
+ case "subtitle":
1743
+ return _jsx("h3", { className: "biab-field__subtitle", children: field.label || "" });
1744
+ case "separator":
1745
+ return _jsx("hr", { className: "biab-field__separator" });
1746
+ case "richtext":
1747
+ // `content` is server-sanitized HTML.
1748
+ return field.content ? (_jsx("div", { className: "biab-field__richtext",
1749
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: server-sanitized
1750
+ dangerouslySetInnerHTML: { __html: field.content } })) : null;
1751
+ case "body_text":
1752
+ // Plain body paragraph (no HTML). Display only.
1753
+ return field.content ? (_jsx("p", { className: "biab-field__body", children: field.content })) : null;
1754
+ case "bullet_list": {
1755
+ const lc = field.listConfig ?? {};
1756
+ const variant = lc.variant ?? "bullets";
1757
+ const items = lc.items ?? [];
1758
+ if (variant === "details") {
1759
+ return (_jsx("div", { className: "biab-field__points", children: items.map((it, i) => (_jsxs("div", { className: "biab-field__point", children: [it.icon && (_jsx("span", { "aria-hidden": "true", className: "biab-field__point-icon", "data-icon": it.icon })), _jsxs("span", { className: "biab-field__point-body", children: [_jsx("span", { className: "biab-field__point-title", children: it.text }), it.description && (_jsx("span", { className: "biab-field__point-desc", children: it.description }))] })] }, `${field.id}-li-${i}`))) }));
1760
+ }
1761
+ return (_jsxs("div", { className: "biab-field__bullets", children: [lc.title && (_jsx("div", { className: "biab-field__bullets-title", children: lc.title })), _jsx("ul", { children: items.map((it, i) => (_jsx("li", { children: it.text }, `${field.id}-li-${i}`))) })] }));
1762
+ }
1763
+ case "table": {
1764
+ const cfg = field.tableConfig ?? {};
1765
+ const columns = cfg.columns ?? [];
1766
+ const rows = cfg.rows ?? [];
1767
+ if (columns.length === 0)
1768
+ return null;
1769
+ return (_jsx("div", { className: "biab-field__table", children: _jsxs("table", { children: [_jsx("thead", { children: _jsx("tr", { children: columns.map((col, ci) => (_jsx("th", { "data-align": col.align ?? "left", "data-highlight": cfg.highlightColumn === ci ? "" : undefined, children: col.header }, `${field.id}-col-${ci}`))) }) }), _jsx("tbody", { children: rows.map((row, ri) => (_jsx("tr", { children: columns.map((col, ci) => {
1770
+ const cell = row.cells?.[ci] ?? "";
1771
+ const check = col.type === "check" ? tableCheckState(cell) : "text";
1772
+ return (_jsx("td", { "data-align": col.align ?? "left", "data-check": check === "text" ? undefined : check, "data-highlight": cfg.highlightColumn === ci ? "" : undefined, children: check === "yes" ? "✓" : check === "no" ? "–" : cell }, `${field.id}-cell-${ri}-${ci}`));
1773
+ }) }, `${field.id}-row-${ri}`))) })] }) }));
1774
+ }
1775
+ case "calculation": {
1776
+ // Value is computed by the controller (non-sensitive) or returned by
1777
+ // the server after submit (sensitive). Read-only display.
1778
+ const cfg = field
1779
+ .calculationConfig;
1780
+ const hasVal = value !== null && value !== undefined && value !== "";
1781
+ if (cfg?.sensitive && !hasVal) {
1782
+ return (_jsx("output", { className: "biab-field__calc biab-field__calc--pending", children: "Calculated on submit." }));
1783
+ }
1784
+ return (_jsx("output", { className: "biab-field__calc", children: hasVal ? formatCalcValue(value, cfg?.display) : "—" }));
1785
+ }
1786
+ case "image": {
1787
+ const cfg = (field["imageConfig"] ?? {});
1788
+ return cfg.src ? (_jsx("img", { alt: cfg.alt || "", className: "biab-field__image", src: cfg.src, style: {
1789
+ width: cfg.width ? `${cfg.width}px` : "auto",
1790
+ height: cfg.height ? `${cfg.height}px` : "auto",
1791
+ maxWidth: "100%",
1792
+ } })) : null;
1793
+ }
1794
+ case "textarea": {
1795
+ const cfg = (field["textareaConfig"] ?? {});
1796
+ return (_jsx("textarea", { autoComplete: autoComplete, className: "biab-textarea biab-field__input", onChange: (e) => set(e.target.value), placeholder: ph, rows: cfg.rows ?? 4, style: cfg.resizable
1797
+ ? { resize: "vertical", minHeight: "6rem", maxHeight: "30rem" }
1798
+ : { resize: "none" }, value: typeof value === "string" ? value : "" }));
1799
+ }
1800
+ case "select": {
1801
+ const opts = field.options ?? [];
1802
+ // ComboBox card (`display: "combobox"`), or any list long enough that
1803
+ // scrolling it is the worse experience. `<input list>` + `<datalist>`
1804
+ // gives native typeahead with no JS and no styling opinion — which is
1805
+ // exactly the SDK's contract.
1806
+ if (shouldSearchOptions(opts.length, field.display)) {
1807
+ const listId = `biab-list-${field.id}`;
1808
+ return (_jsxs(_Fragment, { children: [_jsx("input", { className: "biab-input biab-combobox biab-field__input", list: listId, onChange: (e) => set(e.target.value), placeholder: ph || "Search…", type: "text", value: typeof value === "string" ? value : "" }), _jsx("datalist", { id: listId, children: opts.map((opt) => (_jsx("option", { value: opt }, opt))) })] }));
1809
+ }
1810
+ return (_jsxs("select", { className: "biab-select biab-field__input", onChange: (e) => set(e.target.value), value: typeof value === "string" ? value : "", children: [_jsx("option", { value: "", children: ph || "Select…" }), opts.map((opt) => (_jsx("option", { value: opt, children: opt }, opt)))] }));
1811
+ }
1812
+ case "icon_select":
1813
+ return _jsx(BiabIconSelect, { field: field, set: set, value: value });
1814
+ case "image_input":
1815
+ return _jsx(BiabImageInput, { field: field, set: set, value: value });
1816
+ case "radio":
1817
+ return (_jsx("div", { className: "biab-field__radios", children: (field.options ?? []).map((opt) => (_jsxs("label", { className: "biab-field__radio", children: [_jsx("input", { checked: value === opt, name: field.id, onChange: () => set(opt), type: "radio", value: opt }), _jsx("span", { children: opt })] }, opt))) }));
1818
+ case "checkbox": {
1819
+ const arr = Array.isArray(value) ? value : [];
1820
+ // Multi-Select element: a checkbox (value = string[]) rendered as a native
1821
+ // multiple-select. The SDK ships unstyled HTML; consumers style it.
1822
+ if (field.display === "dropdown") {
1823
+ return (_jsx("select", { className: "biab-select biab-field__input", multiple: true, onChange: (e) => set(Array.from(e.target.selectedOptions).map((o) => o.value)), value: arr, children: (field.options ?? []).map((opt) => (_jsx("option", { value: opt, children: opt }, opt))) }));
1824
+ }
1825
+ return (_jsx("div", { className: "biab-field__checkboxes", children: (field.options ?? []).map((opt) => {
1826
+ const checked = arr.includes(opt);
1827
+ return (_jsxs("label", { className: "biab-field__checkbox", children: [_jsx("input", { checked: checked, onChange: (e) => set(e.target.checked
1828
+ ? [...arr, opt]
1829
+ : arr.filter((x) => x !== opt)), type: "checkbox" }), _jsx("span", { children: opt })] }, opt));
1830
+ }) }));
1831
+ }
1832
+ case "choice_cards": {
1833
+ const cc = field.choiceConfig ?? {};
1834
+ const ccMulti = cc.mode === "multi";
1835
+ const ccOptions = cc.options ?? [];
1836
+ const arr = ccMulti && Array.isArray(value) ? value : [];
1837
+ const single = !ccMulti && typeof value === "string" ? value : "";
1838
+ const toggle = (label) => {
1839
+ if (ccMulti) {
1840
+ set(arr.includes(label)
1841
+ ? arr.filter((x) => x !== label)
1842
+ : [...arr, label]);
1843
+ }
1844
+ else {
1845
+ set(single === label ? "" : label);
1846
+ }
1847
+ };
1848
+ return (_jsx("div", { className: "biab-field__choice-cards", role: ccMulti ? "group" : "radiogroup", children: ccOptions.map((opt) => {
1849
+ const active = ccMulti
1850
+ ? arr.includes(opt.label)
1851
+ : single === opt.label;
1852
+ return (_jsxs("button", { "aria-pressed": active, className: "biab-field__choice-card", "data-active": active ? "" : undefined, "data-product-id": opt.productId || undefined, onClick: () => toggle(opt.label), role: ccMulti ? "checkbox" : "radio", type: "button", children: [opt.imageUrl ? (_jsx("img", { alt: opt.label, className: "biab-field__choice-card-image", src: opt.imageUrl })) : opt.icon ? (_jsx("span", { "aria-hidden": "true", className: "biab-field__choice-card-icon", "data-icon": opt.icon })) : null, _jsxs("span", { className: "biab-field__choice-card-body", children: [_jsxs("span", { className: "biab-field__choice-card-header", children: [_jsx("span", { className: "biab-field__choice-card-label", children: opt.label }), opt.price ? (_jsx("span", { className: "biab-field__choice-card-price", children: opt.price })) : null] }), opt.description ? (_jsx("span", { className: "biab-field__choice-card-description", children: opt.description })) : null, safeHttpHref(opt.href) ? (_jsx("a", { className: "biab-field__choice-card-link", href: safeHttpHref(opt.href), onClick: (e) => e.stopPropagation(), rel: "noopener noreferrer nofollow", target: "_blank", children: "View" })) : null] })] }, opt.label));
1853
+ }) }));
1854
+ }
1855
+ case "boolean":
1856
+ return (_jsxs("label", { className: "biab-field__switch", children: [_jsx("input", { checked: value === true, onChange: (e) => set(e.target.checked), type: "checkbox" }), _jsx("span", { children: field.placeholder || "Yes / No" })] }));
1857
+ case "date":
1858
+ return (_jsx("input", { className: "biab-input biab-field__input", onChange: (e) => set(e.target.value), type: "date", value: typeof value === "string" ? value : "" }));
1859
+ case "time_picker": {
1860
+ const mode = field.timeConfig?.mode ?? "simple";
1861
+ if (mode === "simple") {
1862
+ const format = field.timeConfig?.format ?? "12h";
1863
+ const step = Math.max(field.timeConfig?.stepMinutes ?? 30, 1);
1864
+ const opts = [];
1865
+ for (let h = 0; h < 24; h++) {
1866
+ for (let m = 0; m < 60; m += step) {
1867
+ opts.push(`${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`);
1868
+ }
1869
+ }
1870
+ const fmt = (t) => {
1871
+ if (format === "24h")
1872
+ return t;
1873
+ const [hStr, mStr = "00"] = t.split(":");
1874
+ const h = Number(hStr);
1875
+ const ampm = h < 12 ? "AM" : "PM";
1876
+ const h12 = h % 12 === 0 ? 12 : h % 12;
1877
+ return `${h12}:${mStr} ${ampm}`;
1878
+ };
1879
+ return (_jsxs("select", { className: "biab-select biab-field__input", onChange: (e) => set(e.target.value), value: typeof value === "string" ? value : "", children: [_jsx("option", { value: "", children: ph || "Select time" }), opts.map((o) => (_jsx("option", { value: o, children: fmt(o) }, o)))] }));
1880
+ }
1881
+ const pair = mode === "time_block"
1882
+ ? ["start", "end"]
1883
+ : ["date", "time"];
1884
+ const v = value && typeof value === "object"
1885
+ ? value
1886
+ : {};
1887
+ const inputType = (k) => (k === "date" ? "date" : "time");
1888
+ return (_jsx("div", { className: "biab-field__grid2", children: pair.map((k) => (_jsxs("label", { className: "biab-field__sub", children: [_jsxs("span", { className: "biab-field__sublabel", children: [k[0]?.toUpperCase(), k.slice(1)] }), _jsx("input", { className: "biab-input", onChange: (e) => set({ ...v, [k]: e.target.value }), type: inputType(k), value: v[k] ?? "" })] }, k))) }));
1889
+ }
1890
+ case "date_range":
1891
+ return _jsx(BiabDateRange, { setValue: (next) => set(next), value: value });
1892
+ case "slider": {
1893
+ const min = field.sliderConfig?.min ?? 0;
1894
+ const max = field.sliderConfig?.max ?? 100;
1895
+ const step = field.sliderConfig?.step ?? 1;
1896
+ const unit = field.sliderConfig?.unit;
1897
+ const fmt = (n) => unit === "currency" ? `$${n}` : unit === "percent" ? `${n}%` : `${n}`;
1898
+ const v = typeof value === "number" ? value : min;
1899
+ return (_jsxs("div", { className: "biab-field__slider", children: [_jsxs("div", { className: "biab-field__sliderscale", children: [_jsx("span", { children: fmt(min) }), _jsx("span", { className: "biab-field__slidervalue", children: fmt(v) }), _jsx("span", { children: fmt(max) })] }), _jsx("input", { max: max, min: min, onChange: (e) => set(Number(e.target.value)), step: step, type: "range", value: v })] }));
1900
+ }
1901
+ case "star_rating": {
1902
+ const max = field.starRatingConfig?.maxStars ?? 5;
1903
+ const v = typeof value === "number" ? value : 0;
1904
+ return (_jsx("div", { className: "biab-field__stars", children: Array.from({ length: max }).map((_, i) => (_jsx("button", { "aria-label": `${i + 1} star${i ? "s" : ""}`, className: cx("biab-field__star", i < v && "biab-field__star--on"), onClick: () => set(i + 1), type: "button", children: "\u2605" }, `${field.id}-star-${i}`))) }));
1905
+ }
1906
+ case "happiness_rating": {
1907
+ const v = typeof value === "number" ? value : -1;
1908
+ return (_jsx("div", { className: "biab-field__happiness", children: ["😞", "😕", "😐", "🙂", "😄"].map((emoji, i) => (_jsx("button", { className: cx("biab-field__emoji", i === v && "biab-field__emoji--on"), onClick: () => set(i), type: "button", children: emoji }, emoji))) }));
1909
+ }
1910
+ case "stepper": {
1911
+ const v = typeof value === "number" ? value : 0;
1912
+ return (_jsxs("div", { className: "biab-field__stepper", children: [_jsx("button", { className: "biab-btn biab-btn--ghost", onClick: () => set(v - 1), type: "button", children: "\u2212" }), _jsx("input", { className: "biab-field__steppervalue", inputMode: "numeric", onChange: (e) => {
1913
+ const n = Number(e.target.value.replace(/[^\d-]/g, ""));
1914
+ set(Number.isFinite(n) ? n : 0);
1915
+ }, value: String(v) }), _jsx("button", { className: "biab-btn biab-btn--ghost", onClick: () => set(v + 1), type: "button", children: "+" })] }));
1916
+ }
1917
+ case "address": {
1918
+ const v = value && typeof value === "object"
1919
+ ? value
1920
+ : {};
1921
+ // Per-subfield autofill tokens (street/city/state/zip), suppressed when
1922
+ // autofill is off for this field (global switch or per-field override).
1923
+ const acOff = autoComplete === "off";
1924
+ const rows = [
1925
+ { k: "line1", label: "Address Line 1", ac: "street-address" },
1926
+ { k: "line2", label: "Address Line 2", ac: "address-line2" },
1927
+ { k: "city", label: "City", ac: "address-level2" },
1928
+ ];
1929
+ return (_jsxs("div", { className: "biab-field__address", children: [rows.map(({ k, label, ac }) => (_jsxs("label", { className: "biab-field__sub", children: [_jsx("span", { className: "biab-field__sublabel", children: label }), _jsx("input", { autoComplete: acOff ? "off" : ac, className: "biab-input", onChange: (e) => set({ ...v, [k]: e.target.value }), value: v[k] ?? "" })] }, k))), _jsxs("label", { className: "biab-field__sub", children: [_jsx("span", { className: "biab-field__sublabel", children: "State" }), _jsxs("select", { autoComplete: acOff ? "off" : "address-level1", className: "biab-select", onChange: (e) => set({ ...v, state: e.target.value }), value: v["state"] ?? "", children: [_jsx("option", { value: "", children: "Select state" }), US_STATES.map((s) => (_jsx("option", { value: s.abbr, children: s.name }, s.abbr)))] })] }, "state"), _jsxs("label", { className: "biab-field__sub", children: [_jsx("span", { className: "biab-field__sublabel", children: "Zip" }), _jsx("input", { autoComplete: acOff ? "off" : "postal-code", className: "biab-input", inputMode: "numeric", onChange: (e) =>
1930
+ // Numbers only (allow a single hyphen for ZIP+4).
1931
+ set({ ...v, zip: e.target.value.replace(/[^\d-]/g, "") }), placeholder: "90210", value: v["zip"] ?? "" })] }, "zip")] }));
1932
+ }
1933
+ case "color_picker": {
1934
+ const v = typeof value === "string" ? value : "";
1935
+ const presets = field.options ?? [];
1936
+ const isCustom = v !== "" && !presets.includes(v);
1937
+ return (_jsxs("div", { className: "biab-field__colors", children: [presets.map((color) => (_jsx("button", { className: cx("biab-field__color", v === color && "biab-field__color--on"), onClick: () => set(color), style: { backgroundColor: color }, title: color, type: "button" }, color))), _jsx("label", { className: cx("biab-field__color biab-field__color--custom", isCustom && "biab-field__color--on"), style: isCustom
1938
+ ? { backgroundColor: v }
1939
+ : {
1940
+ background: "conic-gradient(red, yellow, lime, cyan, blue, magenta, red)",
1941
+ }, title: isCustom ? v : "Custom color", children: _jsx("input", { "aria-label": "Custom color", onChange: (e) => set(e.target.value), style: { position: "absolute", inset: 0, opacity: 0 }, type: "color", value: isCustom ? v : "#3b82f6" }) })] }));
1942
+ }
1943
+ case "file_upload": {
1944
+ // DEFERRED: selecting buffers a PendingFileValue; the controller uploads
1945
+ // it during submit(). Validation runs up front; the server re-checks on
1946
+ // presign.
1947
+ const files = Array.isArray(value)
1948
+ ? value
1949
+ : [];
1950
+ const cfg = field.fileConfig ?? {};
1951
+ const maxFiles = cfg.maxFiles ?? 1;
1952
+ const remaining = Math.max(maxFiles - files.length, 0);
1953
+ return (_jsxs("div", { className: "biab-field__files", children: [_jsx("input", { accept: buildAcceptAttr(cfg.allowedTypes), className: "biab-field__fileinput", disabled: remaining === 0, multiple: maxFiles > 1, onChange: (e) => {
1954
+ const list = e.target.files;
1955
+ if (!list)
1956
+ return;
1957
+ const additions = [];
1958
+ for (const file of Array.from(list).slice(0, remaining)) {
1959
+ if (validateFile(file, {
1960
+ allowedTypes: cfg.allowedTypes,
1961
+ maxSize: cfg.maxSize,
1962
+ })) {
1963
+ continue;
1964
+ }
1965
+ additions.push({
1966
+ pending: true,
1967
+ file,
1968
+ name: file.name,
1969
+ size: file.size,
1970
+ });
1971
+ }
1972
+ if (additions.length > 0)
1973
+ set([...files, ...additions]);
1974
+ e.target.value = "";
1975
+ }, type: "file" }), files.length > 0 && (_jsx("ul", { className: "biab-field__filelist", children: files.map((f, i) => (_jsxs("li", { className: "biab-field__fileitem", children: [_jsx("span", { className: "biab-field__filename", children: f.name }), isPendingFileValue(f) ? (_jsxs("span", { className: "biab-field__filepending", children: [" ", "\u00B7 uploads on submit"] })) : null, _jsx("button", { "aria-label": "Remove file", className: "biab-field__fileremove", onClick: () => set(files.filter((_, idx) => idx !== i)), type: "button", children: "\u00D7" })] }, `${f.name}-${i}`))) }))] }));
1976
+ }
1977
+ case "credit_card": {
1978
+ const pc = field.paymentConfig;
1979
+ const deposit = pc?.mode === "deposit";
1980
+ const amount = deposit && pc?.amountCents
1981
+ ? `${(pc.amountCents / 100).toFixed(2)} ${(pc.currency ?? "usd").toUpperCase()}`
1982
+ : null;
1983
+ return (_jsxs("div", { className: "biab-field__card", children: [_jsxs("div", { className: "biab-field__card-row", children: [_jsx("span", { className: "biab-field__card-brand", children: "CARD" }), _jsx("span", { className: "biab-field__card-dots", children: "\u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022" }), _jsx("span", { className: "biab-field__card-mode", children: deposit
1984
+ ? `Deposit${amount ? ` · ${amount}` : ""}`
1985
+ : "Card on file" })] }), pc?.description ? (_jsx("p", { className: "biab-field__card-desc", children: pc.description })) : null, _jsxs("p", { className: "biab-field__card-note", children: ["Secure card entry via Stripe. Initialize Elements with", " ", _jsxs("code", { children: ["biab.forms.paymentIntent(slug, \"", field.id, "\")"] }), " (returns the clientSecret + the org's connected account), confirm, then store the intent id on this field before submit."] })] }));
1986
+ }
1987
+ case "schedule": {
1988
+ if (field.scheduleConfig
1989
+ ?.mode === "auto") {
1990
+ return _jsx(BiabScheduleAuto, { field: field, onChange: set, value: value });
1991
+ }
1992
+ const sc = field.scheduleConfig;
1993
+ const sv = (value && typeof value === "object" ? value : {});
1994
+ const days = sc?.days?.length
1995
+ ? sc.days
1996
+ : ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
1997
+ const windows = sc?.timeWindows?.length
1998
+ ? sc.timeWindows
1999
+ : sc?.granularTimes
2000
+ ? [
2001
+ "morning_8_10",
2002
+ "morning_10_12",
2003
+ "afternoon_12_2",
2004
+ "afternoon_2_4",
2005
+ "evening_4_6",
2006
+ "evening_6_8",
2007
+ ]
2008
+ : ["morning", "afternoon", "evening"];
2009
+ const DAY = {
2010
+ mon: "Mon",
2011
+ tue: "Tue",
2012
+ wed: "Wed",
2013
+ thu: "Thu",
2014
+ fri: "Fri",
2015
+ sat: "Sat",
2016
+ sun: "Sun",
2017
+ };
2018
+ const TIME = {
2019
+ morning: "Morning",
2020
+ afternoon: "Afternoon",
2021
+ evening: "Evening",
2022
+ morning_8_10: "8–10 AM",
2023
+ morning_10_12: "10 AM–12 PM",
2024
+ afternoon_12_2: "12–2 PM",
2025
+ afternoon_2_4: "2–4 PM",
2026
+ evening_4_6: "4–6 PM",
2027
+ evening_6_8: "6–8 PM",
2028
+ };
2029
+ const toggle = (key, item) => {
2030
+ const arr = sv[key] ?? [];
2031
+ set({
2032
+ ...sv,
2033
+ [key]: arr.includes(item)
2034
+ ? arr.filter((x) => x !== item)
2035
+ : [...arr, item],
2036
+ });
2037
+ };
2038
+ return (_jsxs("div", { className: "biab-field__schedule", children: [sc?.allowAsap !== false && (_jsxs("label", { className: "biab-field__schedule-asap", children: [_jsx("input", { checked: sv.asap === true, onChange: (e) => set({ ...sv, asap: e.target.checked }), type: "checkbox" }), " ", "As soon as possible"] })), _jsxs("div", { className: "biab-field__schedule-group", children: [_jsx("span", { className: "biab-field__schedule-label", children: "Preferred days" }), _jsx("div", { className: "biab-field__schedule-chips", children: days.map((d) => (_jsx("button", { className: cx("biab-field__chip", sv.days?.includes(d) && "biab-field__chip--on"), onClick: () => toggle("days", d), type: "button", children: DAY[d] ?? d }, d))) })] }), _jsxs("div", { className: "biab-field__schedule-group", children: [_jsx("span", { className: "biab-field__schedule-label", children: "Preferred times" }), _jsx("div", { className: "biab-field__schedule-chips", children: windows.map((w) => (_jsx("button", { className: cx("biab-field__chip", sv.times?.includes(w) && "biab-field__chip--on"), onClick: () => toggle("times", w), type: "button", children: TIME[w] ?? w }, w))) })] }), sc?.allowNotes !== false && (_jsx("textarea", { className: "biab-input biab-field__schedule-notes", onChange: (e) => set({ ...sv, notes: e.target.value }), placeholder: "Anything we should know about timing?", value: sv.notes ?? "" }))] }));
2039
+ }
2040
+ case "phone": {
2041
+ const phoneIntl = field.phoneConfig?.international === true;
2042
+ const phoneShown = formatPhoneLocal(typeof value === "string" ? value : "", phoneIntl);
2043
+ return (_jsxs("div", { className: "biab-field__phone", children: [field.phoneConfig?.showFlags && (_jsx("span", { "aria-hidden": true, className: "biab-field__flag", children: "\uD83C\uDDFA\uD83C\uDDF8" })), _jsx("input", { autoComplete: autoComplete, className: "biab-input biab-field__input", inputMode: "tel", maxLength: phoneIntl ? 20 : 14, onChange: (e) => set(formatPhoneLocal(e.target.value, phoneIntl)), placeholder: ph || (phoneIntl ? "+1 (555) 000-0000" : "(555) 000-0000"), type: "tel", value: phoneShown })] }));
2044
+ }
2045
+ case "email":
2046
+ return (_jsx("input", { autoComplete: autoComplete, className: "biab-input biab-field__input", onChange: (e) => set(e.target.value), placeholder: ph || "you@example.com", type: "email", value: typeof value === "string" ? value : "" }));
2047
+ case "url": {
2048
+ // Value object: { url, verified }. Visitor edits `url`; server stamps
2049
+ // `verified` on submit.
2050
+ const uv = (value && typeof value === "object" ? value : {});
2051
+ return (_jsx("input", { autoComplete: autoComplete, className: "biab-input biab-field__input", inputMode: "url", onChange: (e) => set({ ...uv, url: e.target.value }), placeholder: ph || "https://example.com", type: "url", value: typeof uv.url === "string" ? uv.url : "" }));
2052
+ }
2053
+ case "number": {
2054
+ const allowDecimal = field.allowDecimal !== false;
2055
+ const noNegative = field.allowNegative === false;
2056
+ return (_jsx("input", { className: "biab-input biab-field__input", inputMode: allowDecimal ? "decimal" : "numeric", min: noNegative ? 0 : undefined, onChange: (e) => {
2057
+ const raw = e.target.value;
2058
+ if (raw === "")
2059
+ return set("");
2060
+ const n = allowDecimal ? Number(raw) : Math.trunc(Number(raw));
2061
+ set(Number.isNaN(n) ? "" : n);
2062
+ }, placeholder: ph, step: allowDecimal ? "any" : 1, type: "number", value: typeof value === "number"
2063
+ ? value
2064
+ : typeof value === "string"
2065
+ ? value
2066
+ : "" }));
2067
+ }
2068
+ case "currency": {
2069
+ const symbol = field.currencyConfig?.symbol || "$";
2070
+ const allowNeg = field.currencyConfig?.allowNegative === true;
2071
+ const allowDec = field.currencyConfig?.allowDecimal !== false;
2072
+ return (_jsxs("div", { className: "biab-field__currency", children: [_jsx("span", { "aria-hidden": "true", className: "biab-field__currency-symbol", children: symbol }), _jsx("input", { className: "biab-input biab-field__input", inputMode: "decimal", min: allowNeg ? undefined : 0, onChange: (e) => {
2073
+ const raw = e.target.value;
2074
+ if (raw === "")
2075
+ return set("");
2076
+ let n = allowDec ? Number(raw) : Math.trunc(Number(raw));
2077
+ if (Number.isNaN(n))
2078
+ return set("");
2079
+ if (!allowNeg && n < 0)
2080
+ n = Math.abs(n);
2081
+ set(n);
2082
+ }, placeholder: ph || "0.00", step: allowDec ? "0.01" : 1, type: "number", value: typeof value === "number" ? value : "" })] }));
2083
+ }
2084
+ case "measurement": {
2085
+ const cfg = field.unitConfig ?? {};
2086
+ const category = isMeasurementCategory(cfg.category)
2087
+ ? cfg.category
2088
+ : "length";
2089
+ const units = cfg.units && cfg.units.length > 0
2090
+ ? cfg.units
2091
+ : unitsForCategory(category);
2092
+ const mode = cfg.mode ?? "select";
2093
+ const defaultUnit = cfg.defaultUnit ?? units[0] ?? "";
2094
+ const allowDec = cfg.allowDecimal !== false;
2095
+ const mv = (value && typeof value === "object" ? value : {});
2096
+ const num = typeof mv.value === "number" ? mv.value : "";
2097
+ const unit = mv.unit ?? defaultUnit;
2098
+ const emit = (nextNum, nextUnit) => {
2099
+ const base = typeof nextNum === "number"
2100
+ ? convertToBase(nextNum, nextUnit, category)
2101
+ : null;
2102
+ set({
2103
+ value: nextNum,
2104
+ unit: nextUnit,
2105
+ base: base ?? undefined,
2106
+ baseUnit: baseUnitFor(category),
2107
+ });
2108
+ };
2109
+ const hint = typeof num === "number" ? formatMeasurement(num, unit, category) : "";
2110
+ return (_jsxs("div", { className: "biab-field__measurement", children: [_jsx("input", { className: "biab-input biab-field__input", inputMode: "decimal", onChange: (e) => {
2111
+ const raw = e.target.value;
2112
+ if (raw === "")
2113
+ return emit("", unit);
2114
+ const n = allowDec ? Number(raw) : Math.trunc(Number(raw));
2115
+ emit(Number.isNaN(n) ? "" : n, unit);
2116
+ }, placeholder: ph || "0", step: allowDec ? "any" : 1, type: "number", value: num }), mode === "fixed" ? (_jsx("span", { "aria-hidden": "true", className: "biab-field__unit", children: unit })) : (_jsx("select", { className: "biab-select biab-field__unit-select", onChange: (e) => emit(num, e.target.value), value: unit, children: units.map((u) => (_jsx("option", { value: u, children: u }, u))) })), hint ? (_jsxs("span", { className: "biab-field__measurement-hint", children: ["\u2248 ", hint] })) : null] }));
2117
+ }
2118
+ default:
2119
+ return (_jsx("input", { autoComplete: autoComplete, className: "biab-input biab-field__input", onChange: (e) => set(e.target.value), placeholder: ph, value: typeof value === "string" ? value : "" }));
2120
+ }
2121
+ }
2122
+ const NO_LABEL_TYPES = new Set([
2123
+ "title",
2124
+ "subtitle",
2125
+ "separator",
2126
+ "richtext",
2127
+ "body_text",
2128
+ "bullet_list",
2129
+ "table",
2130
+ ]);
2131
+ function BiabFieldRow({ field, value, error, setValue, upload, }) {
2132
+ const showLabel = !NO_LABEL_TYPES.has(field.type);
2133
+ return (_jsxs("div", { className: cx("biab-field", `biab-field--${field.type}`, field.required && "biab-field--required", error && "biab-field--error"), "data-biab-field": fieldCssKey(field), "data-biab-type": field.type, "data-field-id": field.id, children: [showLabel && (
2134
+ // A visual field heading, not a control-associated <label> — the field's
2135
+ // inputs render in BiabFieldInput below and carry no shared id to point a
2136
+ // `htmlFor` at, so a <label> here would be an orphan.
2137
+ _jsxs("span", { className: "biab-field__label biab-label", children: [field.label || "", field.required && _jsx("span", { className: "biab-field__required", children: "*" })] })), _jsx(BiabFieldInput, { field: field, setValue: setValue, upload: upload, value: value }), error && _jsx("p", { className: "biab-field__error", children: error.message }), field.helpText && _jsx("p", { className: "biab-field__help", children: field.helpText })] }));
2138
+ }
2139
+ /** Recursively render a field subtree, mirroring FormPreview's `RenderTree`:
2140
+ * input_group (+ concurrent reveal), condition_block, or_condition, flex_row. */
2141
+ function BiabRenderTree({ fields, values, errors, setValue, upload, flat, }) {
2142
+ return (_jsx("div", { className: "biab-tree", children: fields.map((f) => {
2143
+ if (f.type === "multi_populator") {
2144
+ const cfg = f.populatorConfig ?? {};
2145
+ const max = cfg.maxInstances ?? 10;
2146
+ const children = (f.children ?? []);
2147
+ const raw = values[f.id];
2148
+ const instances = Array.isArray(raw) && raw.length > 0
2149
+ ? raw
2150
+ : [{}];
2151
+ const writeInstances = (next) => setValue(f.id, next);
2152
+ return (_jsxs("div", { className: "biab-populator", "data-biab-field": fieldCssKey(f), "data-field-id": f.id, children: [instances.map((inst, idx) => {
2153
+ const scopedSetValue = (childId, v) => {
2154
+ const nextArr = instances.map((x) => ({ ...x }));
2155
+ nextArr[idx] = { ...nextArr[idx], [childId]: v };
2156
+ writeInstances(nextArr);
2157
+ };
2158
+ return (_jsxs("div", { className: "biab-populator__item", children: [_jsxs("div", { className: "biab-populator__item-head", children: [_jsxs("span", { className: "biab-populator__item-label", children: [cfg.itemLabel || f.label || "Entry", " ", idx + 1] }), instances.length > 1 && (_jsx("button", { "aria-label": "Remove entry", className: "biab-populator__remove", onClick: () => writeInstances(instances.filter((_, i) => i !== idx)), type: "button", children: "\u00D7" }))] }), _jsx(BiabRenderTree, { errors: errors, fields: children, setValue: scopedSetValue, upload: upload, values: inst })] }, `${f.id}-inst-${idx}`));
2159
+ }), instances.length < max && (_jsxs("button", { className: "biab-populator__add", onClick: () => writeInstances([...instances, {}]), type: "button", children: ["+ ", cfg.addButtonLabel || "Add another"] }))] }, f.id));
2160
+ }
2161
+ if (f.type === "input_group") {
2162
+ const children = (f.children ?? []);
2163
+ const concurrent = f.concurrentReveal === true;
2164
+ const showGroupLabel = f.showGroupLabel === true;
2165
+ const revealCount = concurrent
2166
+ ? concurrentRevealCount(children, values)
2167
+ : children.length;
2168
+ const shown = concurrent ? children.slice(0, revealCount) : children;
2169
+ return (_jsxs("div", { className: cx("biab-group", flat && "biab-group--flat"), "data-biab-group": fieldCssKey(f), children: [f.label && showGroupLabel && (_jsx("div", { className: "biab-group__label", children: f.label })), _jsx(BiabRenderTree, { errors: errors, fields: shown, setValue: setValue, upload: upload, values: values })] }, f.id));
2170
+ }
2171
+ if (f.type === "condition_block") {
2172
+ if (!evalCondition(f.conditionRule, values))
2173
+ return null;
2174
+ return (_jsx(BiabRenderTree, { errors: errors, fields: (f.children ?? []), setValue: setValue, upload: upload, values: values }, f.id));
2175
+ }
2176
+ if (f.type === "or_condition") {
2177
+ const branches = f.children ?? [];
2178
+ const errored = Boolean(errors[f.id]);
2179
+ const branchLabel = (i) => (i === 0 && f.orConditionConfig?.leftLabel) ||
2180
+ (i === 1 && f.orConditionConfig?.rightLabel) ||
2181
+ branches[i]?.label ||
2182
+ ["Option A", "Option B", "Option C"][i] ||
2183
+ `Option ${i + 1}`;
2184
+ return (_jsxs("div", { className: cx("biab-or", errored && "biab-or--error"), children: [f.label && (_jsxs("div", { className: "biab-or__label", children: [f.label, _jsx("span", { className: "biab-or__hint", children: "Fill at least one side" })] })), _jsx("div", { className: "biab-or__sides", children: branches.flatMap((child, i) => {
2185
+ const side = (_jsxs("div", { className: "biab-or__side", children: [_jsx("div", { className: "biab-or__sidelabel", children: branchLabel(i) }), _jsx(BiabRenderTree, { errors: errors, fields: [child], setValue: setValue, upload: upload, values: values })] }, child.id));
2186
+ if (i === 0)
2187
+ return [side];
2188
+ return [
2189
+ _jsx("div", { className: "biab-or__divider", children: "OR" }, `or-${child.id}`),
2190
+ side,
2191
+ ];
2192
+ }) }), errored && (_jsx("p", { className: "biab-field__error", children: "Fill in at least one side to continue." }))] }, f.id));
2193
+ }
2194
+ if (f.type === "flex_row") {
2195
+ const cfg = (f["flexConfig"] ?? {});
2196
+ return (_jsx("div", { className: "biab-flexrow", style: {
2197
+ display: "flex",
2198
+ justifyContent: cfg.justify === "center"
2199
+ ? "center"
2200
+ : cfg.justify === "end"
2201
+ ? "flex-end"
2202
+ : cfg.justify === "between"
2203
+ ? "space-between"
2204
+ : cfg.justify === "around"
2205
+ ? "space-around"
2206
+ : "flex-start",
2207
+ alignItems: cfg.align === "start"
2208
+ ? "flex-start"
2209
+ : cfg.align === "end"
2210
+ ? "flex-end"
2211
+ : "center",
2212
+ gap: `${cfg.gap ?? 12}px`,
2213
+ flexWrap: "wrap",
2214
+ }, children: (f.children ?? []).map((child) => (_jsx("div", { className: "biab-flexrow__cell", children: _jsx(BiabRenderTree, { errors: errors, fields: [child], setValue: setValue, upload: upload, values: values }) }, child.id))) }, f.id));
2215
+ }
2216
+ return (_jsx(BiabFieldRow, { error: errors[f.id], field: f, setValue: setValue, upload: upload, value: values[f.id] }, f.id));
2217
+ }) }));
2218
+ }
2219
+ export function BiabForm(props) {
2220
+ const providerClient = useBiabFormsClient();
2221
+ // A publishable token (`pk_…`) builds a browser client that calls BIAB
2222
+ // directly — the zero-backend path. Explicit `client` still wins, then the
2223
+ // token client, then a surrounding provider.
2224
+ const tokenClient = useMemo(() => props.token && props.siteId
2225
+ ? createBiabClient({
2226
+ apiKey: props.token,
2227
+ siteId: props.siteId,
2228
+ baseUrl: props.baseUrl ?? DEFAULT_PACKAGE_BASE_URL,
2229
+ })
2230
+ : null, [props.token, props.siteId, props.baseUrl]);
2231
+ const client = props.client ?? tokenClient ?? providerClient ?? undefined;
2232
+ const submitLabel = props.submitLabel ?? "Submit";
2233
+ // Latest callbacks without re-creating the controller on every render.
2234
+ const cbRef = useRef({
2235
+ onSuccess: props.onSuccess,
2236
+ onError: props.onError,
2237
+ onSubmit: props.onSubmit,
2238
+ });
2239
+ cbRef.current = {
2240
+ onSuccess: props.onSuccess,
2241
+ onError: props.onError,
2242
+ onSubmit: props.onSubmit,
2243
+ };
2244
+ // Serialize submitOptions so a fresh-but-equal object literal (the common
2245
+ // consumer case) doesn't recreate the controller and re-fetch every render.
2246
+ const submitOptionsKey = JSON.stringify(props.submitOptions ?? {});
2247
+ // One controller per (client, slug, schema, submit config). The controller is
2248
+ // the single source of truth; React only renders its snapshots. `submitOptions`
2249
+ // is keyed by its serialized value (`submitOptionsKey`); the spread reads the
2250
+ // live object, so the object identity itself is intentionally not a dep.
2251
+ // biome-ignore lint/correctness/useExhaustiveDependencies: keyed by submitOptionsKey
2252
+ const controller = useMemo(() => createFormController({
2253
+ ...(client ? { client } : {}),
2254
+ ...(props.slug ? { slug: props.slug } : {}),
2255
+ ...(props.schema ? { schema: props.schema } : {}),
2256
+ submitOptions: {
2257
+ ...props.submitOptions,
2258
+ ...(props.dryRun ? { dryRun: true } : {}),
2259
+ },
2260
+ onSuccess: (r) => cbRef.current.onSuccess?.(r),
2261
+ onError: (r) => cbRef.current.onError?.(r),
2262
+ }), [client, props.slug, props.schema, props.dryRun, submitOptionsKey]);
2263
+ useEffect(() => {
2264
+ void controller.load();
2265
+ return () => controller.destroy();
2266
+ }, [controller]);
2267
+ const snap = useFormSnapshot(controller);
2268
+ const upload = useMemo(() => {
2269
+ if (!client?.forms.uploadFile)
2270
+ return undefined;
2271
+ return (fieldId, file) => controller.uploadFile(fieldId, file);
2272
+ }, [client, controller]);
2273
+ // Auto-`schedule` slot fetcher, derived from the client + slug and provided
2274
+ // via context so the deep field renderer needn't thread it.
2275
+ const fetchScheduleSlots = useMemo(() => {
2276
+ const slug = props.slug;
2277
+ const fn = client?.forms.scheduleSlots;
2278
+ if (!fn || !slug)
2279
+ return undefined;
2280
+ return (input) => fn(slug, input).then((r) => r.slots);
2281
+ }, [client, props.slug]);
2282
+ // Autofill config provided to the field tree. Keyed by the serialized
2283
+ // overrides so a fresh-but-equal `fieldAutoComplete` literal doesn't churn.
2284
+ const fieldAutoCompleteKey = JSON.stringify(props.fieldAutoComplete ?? null);
2285
+ const autocompleteConfig = useMemo(
2286
+ // biome-ignore lint/correctness/useExhaustiveDependencies: keyed by fieldAutoCompleteKey
2287
+ () => ({
2288
+ ...(props.autoComplete !== undefined
2289
+ ? { enabled: props.autoComplete }
2290
+ : {}),
2291
+ ...(props.fieldAutoComplete
2292
+ ? { overrides: props.fieldAutoComplete }
2293
+ : {}),
2294
+ }), [props.autoComplete, fieldAutoCompleteKey]);
2295
+ // Exit-confirm guard: warn before leaving a dirty, unsubmitted form when the
2296
+ // org enabled it. Browsers show a generic native prompt; the configured
2297
+ // message is also on `snap.settings.exitConfirm.message` for a custom modal.
2298
+ const exitConfirm = snap.settings?.exitConfirm;
2299
+ const exitDirty = Object.values(snap.values).some((v) => Array.isArray(v) ? v.length > 0 : v != null && v !== "" && v !== false);
2300
+ useEffect(() => {
2301
+ if (!exitConfirm?.enabled ||
2302
+ snap.submitted ||
2303
+ !exitDirty ||
2304
+ typeof window === "undefined") {
2305
+ return;
2306
+ }
2307
+ const onBeforeUnload = (e) => {
2308
+ e.preventDefault();
2309
+ e.returnValue = exitConfirm.message || "";
2310
+ };
2311
+ window.addEventListener("beforeunload", onBeforeUnload);
2312
+ return () => window.removeEventListener("beforeunload", onBeforeUnload);
2313
+ }, [exitConfirm?.enabled, exitConfirm?.message, snap.submitted, exitDirty]);
2314
+ const rootClass = cx("biab-form", props.className, props.class);
2315
+ if (snap.status === "loading" || snap.status === "idle") {
2316
+ return (_jsx("div", { className: rootClass, style: props.style, children: props.loadingFallback ?? (_jsx("div", { className: "biab-form__loading", children: "Loading\u2026" })) }));
2317
+ }
2318
+ if (snap.status === "error") {
2319
+ const message = snap.loadError ?? "Failed to load the form.";
2320
+ return (_jsx("div", { className: rootClass, style: props.style, children: props.errorFallback ? (props.errorFallback(message)) : (_jsx("div", { className: "biab-form__error", children: message })) }));
2321
+ }
2322
+ if (snap.submitted && snap.result?.ok) {
2323
+ const ok = snap.result;
2324
+ return (_jsx("div", { className: rootClass, style: props.style, children: props.successFallback ? (props.successFallback(ok)) : (_jsx("div", { className: "biab-form__success", children: "Thanks \u2014 your response was received." })) }));
2325
+ }
2326
+ const { settings } = snap;
2327
+ const consecutive = settings.isConsecutive && snap.stepCount > 1;
2328
+ const animClass = settings.animation === "fade"
2329
+ ? "biab-anim--fade"
2330
+ : settings.animation === "scale"
2331
+ ? "biab-anim--scale"
2332
+ : settings.animation === "slide"
2333
+ ? snap.forward
2334
+ ? "biab-anim--slide-fwd"
2335
+ : "biab-anim--slide-back"
2336
+ : settings.animation === "scroll"
2337
+ ? "biab-anim--scroll"
2338
+ : "";
2339
+ const handleSubmit = async () => {
2340
+ const res = await controller.submit();
2341
+ if (res.ok) {
2342
+ cbRef.current.onSubmit?.(snap.values);
2343
+ return;
2344
+ }
2345
+ // Fix-it wizard: route to the first errored field's step (consecutive) and
2346
+ // scroll it into view, so cross-step / file-upload errors are never hidden.
2347
+ const firstId = res.issues?.[0]?.fieldId ?? snap.errorList[0]?.id;
2348
+ if (!firstId)
2349
+ return;
2350
+ let delay = 60;
2351
+ if (consecutive) {
2352
+ const si = snap.steps.findIndex((s) => flattenValueFields(s).some((f) => f.id === firstId));
2353
+ if (si >= 0 && si !== snap.step) {
2354
+ controller.goToStep(si);
2355
+ delay = 200; // let the destination step mount before scrolling
2356
+ }
2357
+ }
2358
+ if (typeof document !== "undefined") {
2359
+ setTimeout(() => {
2360
+ const el = document.querySelector(`[data-field-id="${firstId}"]`);
2361
+ el?.scrollIntoView({ behavior: "smooth", block: "center" });
2362
+ }, delay);
2363
+ }
2364
+ };
2365
+ return (_jsx(FetchScheduleSlotsContext.Provider, { value: fetchScheduleSlots, children: _jsx(AutocompleteContext.Provider, { value: autocompleteConfig, children: _jsxs("div", { className: rootClass, style: props.style, children: [snap.orgIcon && (_jsx("img", { alt: "", className: "biab-form__orgicon", src: snap.orgIcon })), (snap.title || snap.description) && (_jsxs("div", { className: "biab-form__head", children: [snap.title && _jsx("h2", { className: "biab-form__title", children: snap.title }), snap.description && (_jsx("p", { className: "biab-form__desc", children: snap.description }))] })), consecutive &&
2366
+ settings.showProgress &&
2367
+ settings.progressStyle !== "bar" && (_jsx("div", { className: "biab-progress biab-progress--steps", children: snap.steps.map((s, i) => {
2368
+ const f = s[0];
2369
+ const title = f?.stepTitle || f?.label || `Step ${i + 1}`;
2370
+ const done = i < snap.step;
2371
+ const current = i === snap.step;
2372
+ return (_jsxs(Fragment, { children: [i > 0 && (_jsx("span", { className: cx("biab-progress__line", i <= snap.step && "biab-progress__line--done") })), _jsxs("button", { className: cx("biab-progress__step", current && "biab-progress__step--current", done && "biab-progress__step--done"), onClick: () => controller.goToStep(i), type: "button", children: [_jsx("span", { className: "biab-progress__num", children: done ? "✓" : i + 1 }), _jsx("span", { className: "biab-progress__title", children: title })] })] }, `step-${i}`));
2373
+ }) })), consecutive &&
2374
+ settings.showProgress &&
2375
+ settings.progressStyle === "bar" && (_jsxs("div", { className: "biab-progress biab-progress--bar", children: [_jsxs("span", { className: "biab-progress__count", children: [snap.step + 1, " / ", snap.stepCount] }), _jsx("div", { className: "biab-progress__track", children: _jsx("div", { className: "biab-progress__fill", style: { width: `${snap.progressPercent}%` } }) })] })), snap.errorList.length > 0 && (_jsxs("div", { className: "biab-form__notice", role: "alert", children: [_jsxs("div", { className: "biab-form__notice-title", children: ["Please complete the highlighted field", snap.errorList.length === 1 ? "" : "s", "."] }), _jsx("div", { className: "biab-form__notice-list", children: snap.errorList.map((e) => e.label).join(", ") })] })), _jsx("div", { className: cx("biab-form__body", animClass), children: _jsx(BiabRenderTree, { errors: snap.errors, fields: snap.visibleFields, flat: consecutive, setValue: (id, v) => controller.setValue(id, v), upload: upload, values: snap.values }) }, `biab-step-${snap.step}`), (!consecutive || snap.isLastStep) &&
2376
+ (snap.schema?.actions ?? [])
2377
+ .filter((a) => a.type === "grant_coupon" &&
2378
+ (a.coupon?.notice || a.coupon?.code))
2379
+ .map((a, i) => {
2380
+ const code = a.coupon?.code ?? "";
2381
+ const text = a.coupon?.notice
2382
+ ? a.coupon.notice.replace(/\{code\}/g, code)
2383
+ : `Coupon ${code} will be applied to this submission.`;
2384
+ return (_jsxs("div", { className: "biab-coupon-notice", children: [_jsx("p", { className: "biab-coupon-notice__text", children: text }), a.coupon?.finePrint && (_jsx("p", { className: "biab-coupon-notice__fine", children: a.coupon.finePrint }))] }, `coupon-${i}`));
2385
+ }), consecutive ? (_jsxs("div", { className: "biab-nav", children: [_jsx("button", { className: "biab-btn biab-btn--back biab-btn--ghost", disabled: snap.isFirstStep, onClick: () => controller.back(), type: "button", children: "Back" }), !snap.isLastStep ? (_jsx("button", { className: "biab-btn biab-btn--next", disabled: !snap.canAdvance, onClick: () => controller.next(), type: "button", children: "Next" })) : (_jsx("button", { className: "biab-btn biab-btn--submit", disabled: snap.submitting || snap.uploading, onClick: () => void handleSubmit(), type: "button", children: snap.submitting ? "Sending…" : submitLabel }))] })) : (_jsx("div", { className: "biab-nav", children: _jsx("button", { className: "biab-btn biab-btn--submit", disabled: snap.submitting || snap.uploading, onClick: () => void handleSubmit(), type: "button", children: snap.submitting ? "Sending…" : submitLabel }) })), snap.result && !snap.result.ok && snap.errorList.length === 0 && (_jsx("div", { className: "biab-form__error", role: "alert", children: snap.result.message }))] }) }) }));
2386
+ }
2387
+ function SocialGlyph({ iconSlug, color, size, className, label, }) {
2388
+ if (iconSlug) {
2389
+ const src = color
2390
+ ? `https://cdn.simpleicons.org/${iconSlug}/${encodeURIComponent(color)}`
2391
+ : `https://cdn.simpleicons.org/${iconSlug}`;
2392
+ return (
2393
+ // biome-ignore lint: remote brand icon from the simple-icons CDN
2394
+ _jsx("img", { alt: label, className: className, height: size, loading: "lazy", src: src, width: size }));
2395
+ }
2396
+ return (_jsxs("svg", { "aria-hidden": true, className: className, fill: "none", height: size, stroke: "currentColor", strokeWidth: 1.8, viewBox: "0 0 24 24", width: size, children: [_jsx("title", { children: label }), _jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })] }));
2397
+ }
2398
+ /**
2399
+ * Render every social profile on the company as an icon + link, in canonical
2400
+ * order, resolving BIAB's exact keys (`meta`, `twitterX`, …) for you. Branded
2401
+ * glyphs come from the simple-icons CDN; platforms without one fall back to a
2402
+ * globe. Returns `null` when no socials are set.
2403
+ */
2404
+ export function SocialLinks({ socials, color, size = 20, className, linkClassName, iconClassName, }) {
2405
+ const rows = resolveSocialProfiles(socials);
2406
+ if (rows.length === 0)
2407
+ return null;
2408
+ return (_jsx("ul", { className: className, style: {
2409
+ display: "flex",
2410
+ flexWrap: "wrap",
2411
+ alignItems: "center",
2412
+ gap: "0.75rem",
2413
+ listStyle: "none",
2414
+ margin: 0,
2415
+ padding: 0,
2416
+ }, children: rows.map((row) => (_jsx("li", { children: _jsx("a", { "aria-label": row.label, className: linkClassName, href: row.url, rel: "noopener noreferrer", target: "_blank", title: row.label, children: _jsx(SocialGlyph, { className: iconClassName, color: color, iconSlug: row.iconSlug, label: row.label, size: size }) }) }, row.key))) }));
2417
+ }
2418
+ //# sourceMappingURL=react.js.map