@forms.expert/sdk 0.1.0

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.
@@ -0,0 +1,1014 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // react/index.ts
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ FormsExpertForm: () => FormsExpertForm,
24
+ FormsProvider: () => FormsProvider,
25
+ useForm: () => useForm,
26
+ useFormsSDK: () => useFormsSDK
27
+ });
28
+ module.exports = __toCommonJS(react_exports);
29
+
30
+ // react/use-form.tsx
31
+ var import_react = require("react");
32
+
33
+ // core/types.ts
34
+ var FormsError = class extends Error {
35
+ constructor(message, code, statusCode, retryAfter) {
36
+ super(message);
37
+ this.code = code;
38
+ this.statusCode = statusCode;
39
+ this.retryAfter = retryAfter;
40
+ this.name = "FormsError";
41
+ }
42
+ };
43
+ var FormValidationError = class extends Error {
44
+ constructor(errors) {
45
+ super("Validation failed");
46
+ this.errors = errors;
47
+ this.name = "FormValidationError";
48
+ }
49
+ };
50
+
51
+ // core/api-client.ts
52
+ var FormsApiClient = class {
53
+ constructor(config) {
54
+ this.apiKey = config.apiKey;
55
+ this.resourceId = config.resourceId;
56
+ this.baseUrl = (config.baseUrl || "https://api.formsapp.io/api/v1").replace(/\/$/, "");
57
+ }
58
+ /**
59
+ * Build URL with token query parameter
60
+ */
61
+ buildUrl(path) {
62
+ const separator = path.includes("?") ? "&" : "?";
63
+ return `${this.baseUrl}${path}${separator}token=${encodeURIComponent(this.apiKey)}`;
64
+ }
65
+ /**
66
+ * Make an API request
67
+ */
68
+ async request(method, path, body) {
69
+ const url = this.buildUrl(path);
70
+ const response = await fetch(url, {
71
+ method,
72
+ headers: {
73
+ "Content-Type": "application/json"
74
+ },
75
+ body: body ? JSON.stringify(body) : void 0
76
+ });
77
+ const data = await response.json();
78
+ if (!response.ok) {
79
+ throw new FormsError(
80
+ data.message || "Request failed",
81
+ data.code || "UNKNOWN_ERROR",
82
+ response.status,
83
+ data.retryAfter
84
+ );
85
+ }
86
+ return data;
87
+ }
88
+ /**
89
+ * Check if form is active and get configuration
90
+ */
91
+ async isActive(slug) {
92
+ return this.request("GET", `/f/${this.resourceId}/${slug}/is-active`);
93
+ }
94
+ /**
95
+ * Validate form data without submitting
96
+ */
97
+ async validate(slug, data) {
98
+ return this.request("POST", `/f/${this.resourceId}/${slug}/validate`, {
99
+ data
100
+ });
101
+ }
102
+ /**
103
+ * Submit form data (supports files)
104
+ */
105
+ async submit(slug, data, options) {
106
+ const url = this.buildUrl(`/f/${this.resourceId}/${slug}`);
107
+ const hasFiles = Object.values(data).some(
108
+ (v) => v instanceof File || v instanceof FileList && v.length > 0
109
+ );
110
+ if (hasFiles || options?.onProgress) {
111
+ return this.submitWithFormData(url, data, options);
112
+ }
113
+ return this.request("POST", `/f/${this.resourceId}/${slug}`, {
114
+ data,
115
+ pageUrl: options?.pageUrl || (typeof window !== "undefined" ? window.location.href : void 0),
116
+ captchaToken: options?.captchaToken
117
+ });
118
+ }
119
+ /**
120
+ * Submit with FormData (for file uploads with progress tracking)
121
+ */
122
+ submitWithFormData(url, data, options) {
123
+ return new Promise((resolve, reject) => {
124
+ const formData = new FormData();
125
+ for (const [key, value] of Object.entries(data)) {
126
+ if (value instanceof File) {
127
+ formData.append(key, value);
128
+ } else if (value instanceof FileList) {
129
+ Array.from(value).forEach((file) => formData.append(key, file));
130
+ } else if (value !== void 0 && value !== null) {
131
+ formData.append(`data[${key}]`, String(value));
132
+ }
133
+ }
134
+ const pageUrl = options?.pageUrl || (typeof window !== "undefined" ? window.location.href : "");
135
+ if (pageUrl) {
136
+ formData.append("pageUrl", pageUrl);
137
+ }
138
+ if (options?.captchaToken) {
139
+ formData.append("captchaToken", options.captchaToken);
140
+ }
141
+ const xhr = new XMLHttpRequest();
142
+ if (options?.onProgress) {
143
+ xhr.upload.addEventListener("progress", (event) => {
144
+ if (event.lengthComputable) {
145
+ options.onProgress({
146
+ loaded: event.loaded,
147
+ total: event.total,
148
+ percentage: Math.round(event.loaded / event.total * 100)
149
+ });
150
+ }
151
+ });
152
+ }
153
+ xhr.addEventListener("load", () => {
154
+ try {
155
+ const response = JSON.parse(xhr.responseText);
156
+ if (xhr.status >= 200 && xhr.status < 300) {
157
+ resolve(response);
158
+ } else {
159
+ reject(new FormsError(
160
+ response.message || "Submission failed",
161
+ response.code || "UNKNOWN_ERROR",
162
+ xhr.status,
163
+ response.retryAfter
164
+ ));
165
+ }
166
+ } catch {
167
+ reject(new FormsError("Invalid response", "PARSE_ERROR", xhr.status));
168
+ }
169
+ });
170
+ xhr.addEventListener("error", () => {
171
+ reject(new FormsError("Network error", "NETWORK_ERROR", 0));
172
+ });
173
+ xhr.addEventListener("abort", () => {
174
+ reject(new FormsError("Request aborted", "ABORTED", 0));
175
+ });
176
+ xhr.open("POST", url);
177
+ xhr.send(formData);
178
+ });
179
+ }
180
+ /**
181
+ * Track a form view (for analytics completion rate)
182
+ */
183
+ async trackView(slug) {
184
+ const url = this.buildUrl(`/f/${this.resourceId}/${slug}/view`);
185
+ await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" } }).catch(() => {
186
+ });
187
+ }
188
+ /**
189
+ * Get resource ID
190
+ */
191
+ getResourceId() {
192
+ return this.resourceId;
193
+ }
194
+ /**
195
+ * Get base URL
196
+ */
197
+ getBaseUrl() {
198
+ return this.baseUrl;
199
+ }
200
+ };
201
+
202
+ // core/forms-sdk.ts
203
+ var FormHandler = class {
204
+ constructor(apiClient, slug, options = {}) {
205
+ this.config = null;
206
+ this.apiClient = apiClient;
207
+ this.slug = slug;
208
+ this.options = options;
209
+ }
210
+ /**
211
+ * Initialize form handler and fetch configuration
212
+ */
213
+ async initialize() {
214
+ this.config = await this.apiClient.isActive(this.slug);
215
+ if (this.options.trackViews) {
216
+ this.apiClient.trackView(this.slug);
217
+ }
218
+ return this.config;
219
+ }
220
+ /**
221
+ * Get cached form configuration
222
+ */
223
+ getConfig() {
224
+ return this.config;
225
+ }
226
+ /**
227
+ * Check if form is active
228
+ */
229
+ isActive() {
230
+ return this.config?.active ?? false;
231
+ }
232
+ /**
233
+ * Check if captcha is required
234
+ */
235
+ requiresCaptcha() {
236
+ return this.config?.settings?.captcha?.enabled ?? false;
237
+ }
238
+ /**
239
+ * Get captcha provider
240
+ */
241
+ getCaptchaProvider() {
242
+ return this.config?.settings?.captcha?.provider;
243
+ }
244
+ /**
245
+ * Get form schema
246
+ */
247
+ getSchema() {
248
+ return this.config?.schema;
249
+ }
250
+ /**
251
+ * Validate form data
252
+ */
253
+ async validate(data) {
254
+ return this.apiClient.validate(this.slug, data);
255
+ }
256
+ /**
257
+ * Submit form data
258
+ */
259
+ async submit(data, options) {
260
+ this.options.onSubmitStart?.();
261
+ try {
262
+ if (this.config?.mode === "schema") {
263
+ const validation = await this.validate(data);
264
+ if (!validation.valid) {
265
+ this.options.onValidationError?.(validation.errors);
266
+ throw new FormValidationError(validation.errors);
267
+ }
268
+ }
269
+ const response = await this.apiClient.submit(this.slug, data, options);
270
+ this.options.onSubmitSuccess?.(response);
271
+ return response;
272
+ } catch (error) {
273
+ if (error instanceof FormsError) {
274
+ this.options.onSubmitError?.(error);
275
+ }
276
+ throw error;
277
+ }
278
+ }
279
+ /**
280
+ * Get success message from config
281
+ */
282
+ getSuccessMessage() {
283
+ return this.config?.settings?.successMessage || "Form submitted successfully!";
284
+ }
285
+ /**
286
+ * Get redirect URL from config
287
+ */
288
+ getRedirectUrl() {
289
+ return this.config?.settings?.redirectUrl;
290
+ }
291
+ };
292
+ var FormsSDK = class {
293
+ constructor(config) {
294
+ this.apiClient = new FormsApiClient(config);
295
+ }
296
+ /**
297
+ * Check if form is active and get configuration
298
+ */
299
+ async isActive(slug) {
300
+ return this.apiClient.isActive(slug);
301
+ }
302
+ /**
303
+ * Validate form data without submitting
304
+ */
305
+ async validate(slug, data) {
306
+ return this.apiClient.validate(slug, data);
307
+ }
308
+ /**
309
+ * Submit form data
310
+ */
311
+ async submit(slug, data, options) {
312
+ return this.apiClient.submit(slug, data, options);
313
+ }
314
+ /**
315
+ * Create a form handler for a specific form
316
+ */
317
+ form(slug, options) {
318
+ return new FormHandler(this.apiClient, slug, options);
319
+ }
320
+ /**
321
+ * Track a form view (for analytics completion rate)
322
+ */
323
+ async trackView(slug) {
324
+ return this.apiClient.trackView(slug);
325
+ }
326
+ /**
327
+ * Submit with retry logic for rate limits
328
+ */
329
+ async submitWithRetry(slug, data, options) {
330
+ const maxRetries = options?.maxRetries ?? 3;
331
+ let lastError = null;
332
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
333
+ try {
334
+ return await this.submit(slug, data, options);
335
+ } catch (error) {
336
+ lastError = error;
337
+ if (error instanceof FormsError) {
338
+ if ([
339
+ "VALIDATION_ERROR",
340
+ "CAPTCHA_REQUIRED",
341
+ "ORIGIN_NOT_ALLOWED"
342
+ ].includes(error.code)) {
343
+ throw error;
344
+ }
345
+ if (error.code.includes("RATE_LIMIT")) {
346
+ const retryAfter = error.retryAfter || Math.pow(2, attempt) * 1e3;
347
+ await new Promise((resolve) => setTimeout(resolve, retryAfter));
348
+ continue;
349
+ }
350
+ }
351
+ await new Promise(
352
+ (resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1e3)
353
+ );
354
+ }
355
+ }
356
+ throw lastError;
357
+ }
358
+ };
359
+
360
+ // react/use-form.tsx
361
+ var import_jsx_runtime = require("react/jsx-runtime");
362
+ var FormsContext = (0, import_react.createContext)(null);
363
+ function FormsProvider({ config, children }) {
364
+ const sdk = (0, import_react.useMemo)(() => new FormsSDK(config), [config]);
365
+ const value = (0, import_react.useMemo)(() => ({ sdk, isReady: true }), [sdk]);
366
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FormsContext.Provider, { value, children });
367
+ }
368
+ function formatBytes(bytes) {
369
+ if (bytes === 0) return "0 Bytes";
370
+ const k = 1024;
371
+ const sizes = ["Bytes", "KB", "MB", "GB"];
372
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
373
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
374
+ }
375
+ function useFormsSDK() {
376
+ const context = (0, import_react.useContext)(FormsContext);
377
+ if (!context) {
378
+ throw new Error("useFormsSDK must be used within a FormsProvider");
379
+ }
380
+ return context.sdk;
381
+ }
382
+ function useForm(options) {
383
+ const context = (0, import_react.useContext)(FormsContext);
384
+ const sdk = (0, import_react.useMemo)(() => {
385
+ if (options.config) {
386
+ return new FormsSDK(options.config);
387
+ }
388
+ if (!context) {
389
+ throw new Error("Either provide config to useForm or use FormsProvider");
390
+ }
391
+ return context.sdk;
392
+ }, [options.config, context]);
393
+ const [config, setConfig] = (0, import_react.useState)(null);
394
+ const [isLoading, setIsLoading] = (0, import_react.useState)(false);
395
+ const [isInitializing, setIsInitializing] = (0, import_react.useState)(false);
396
+ const [isSubmitted, setIsSubmitted] = (0, import_react.useState)(false);
397
+ const [errors, setErrors] = (0, import_react.useState)({});
398
+ const [fileErrors, setFileErrors] = (0, import_react.useState)([]);
399
+ const [values, setValues] = (0, import_react.useState)({});
400
+ const [error, setError] = (0, import_react.useState)(null);
401
+ const [uploadProgress, setUploadProgress] = (0, import_react.useState)(null);
402
+ const initialize = (0, import_react.useCallback)(async () => {
403
+ setIsInitializing(true);
404
+ try {
405
+ const formConfig = await sdk.isActive(options.slug);
406
+ setConfig(formConfig);
407
+ if (options.trackViews) {
408
+ sdk.trackView(options.slug);
409
+ }
410
+ return formConfig;
411
+ } finally {
412
+ setIsInitializing(false);
413
+ }
414
+ }, [sdk, options.slug, options.trackViews]);
415
+ (0, import_react.useEffect)(() => {
416
+ if (options.autoInit !== false) {
417
+ initialize();
418
+ }
419
+ }, [initialize, options.autoInit]);
420
+ const setValue = (0, import_react.useCallback)((name, value) => {
421
+ setValues((prev) => ({ ...prev, [name]: value }));
422
+ setErrors((prev) => {
423
+ if (prev[name]) {
424
+ const { [name]: _, ...rest } = prev;
425
+ return rest;
426
+ }
427
+ return prev;
428
+ });
429
+ }, []);
430
+ const setValuesHandler = (0, import_react.useCallback)((newValues) => {
431
+ setValues((prev) => ({ ...prev, ...newValues }));
432
+ }, []);
433
+ const validate = (0, import_react.useCallback)(async () => {
434
+ const result = await sdk.validate(options.slug, values);
435
+ if (!result.valid) {
436
+ const errorMap = result.errors.reduce(
437
+ (acc, err) => ({ ...acc, [err.field]: err.message }),
438
+ {}
439
+ );
440
+ setErrors(errorMap);
441
+ }
442
+ return result.valid;
443
+ }, [sdk, options.slug, values]);
444
+ const submit = (0, import_react.useCallback)(
445
+ async (captchaToken) => {
446
+ setIsLoading(true);
447
+ setErrors({});
448
+ setFileErrors([]);
449
+ setError(null);
450
+ setUploadProgress(null);
451
+ try {
452
+ const submitData = config?.settings?.honeypot ? { ...values, _hp: "" } : values;
453
+ const response = await sdk.submit(options.slug, submitData, {
454
+ captchaToken,
455
+ onProgress: setUploadProgress
456
+ });
457
+ setIsSubmitted(true);
458
+ options.onSuccess?.(response);
459
+ return response;
460
+ } catch (err) {
461
+ if (err instanceof FormValidationError) {
462
+ const errorMap = err.errors.reduce(
463
+ (acc, e) => ({ ...acc, [e.field]: e.message }),
464
+ {}
465
+ );
466
+ setErrors(errorMap);
467
+ options.onValidationError?.(err.errors);
468
+ } else {
469
+ setError(err);
470
+ options.onError?.(err);
471
+ }
472
+ return null;
473
+ } finally {
474
+ setIsLoading(false);
475
+ setUploadProgress(null);
476
+ }
477
+ },
478
+ [sdk, options, values]
479
+ );
480
+ const reset = (0, import_react.useCallback)(() => {
481
+ setValues({});
482
+ setErrors({});
483
+ setFileErrors([]);
484
+ setError(null);
485
+ setIsSubmitted(false);
486
+ setIsLoading(false);
487
+ setUploadProgress(null);
488
+ }, []);
489
+ const clearErrors = (0, import_react.useCallback)(() => {
490
+ setErrors({});
491
+ setFileErrors([]);
492
+ }, []);
493
+ const validateFiles = (0, import_react.useCallback)((files) => {
494
+ const errors2 = [];
495
+ const maxSize = config?.settings?.maxAttachmentSize ?? 10 * 1024 * 1024;
496
+ const maxCount = config?.settings?.maxAttachments ?? 5;
497
+ if (files.length > maxCount) {
498
+ errors2.push({
499
+ field: "attachments",
500
+ file: "",
501
+ error: "count",
502
+ message: `Maximum ${maxCount} files allowed`
503
+ });
504
+ }
505
+ files.forEach((file) => {
506
+ if (file.size > maxSize) {
507
+ errors2.push({
508
+ field: "attachments",
509
+ file: file.name,
510
+ error: "size",
511
+ message: `File "${file.name}" exceeds maximum size of ${formatBytes(maxSize)}`
512
+ });
513
+ }
514
+ });
515
+ setFileErrors(errors2);
516
+ return errors2;
517
+ }, [config?.settings?.maxAttachmentSize, config?.settings?.maxAttachments]);
518
+ const allowsAttachments = config?.settings?.allowAttachments ?? false;
519
+ const maxAttachments = config?.settings?.maxAttachments ?? 5;
520
+ const maxAttachmentSize = config?.settings?.maxAttachmentSize ?? 10 * 1024 * 1024;
521
+ return {
522
+ config,
523
+ schema: config?.schema ?? null,
524
+ isLoading,
525
+ isInitializing,
526
+ isSubmitted,
527
+ isSuccess: isSubmitted,
528
+ errors,
529
+ fileErrors,
530
+ error,
531
+ values,
532
+ uploadProgress,
533
+ initialize,
534
+ setValue,
535
+ setValues: setValuesHandler,
536
+ validate,
537
+ validateFiles,
538
+ submit,
539
+ reset,
540
+ clearErrors,
541
+ requiresCaptcha: config?.settings?.captcha?.enabled ?? false,
542
+ captchaProvider: config?.settings?.captcha?.provider,
543
+ captchaSiteKey: config?.settings?.captcha?.siteKey,
544
+ honeypotEnabled: config?.settings?.honeypot ?? false,
545
+ allowsAttachments,
546
+ maxAttachments,
547
+ maxAttachmentSize
548
+ };
549
+ }
550
+
551
+ // react/forms-expert-form.tsx
552
+ var import_react2 = require("react");
553
+ var import_jsx_runtime2 = require("react/jsx-runtime");
554
+ var defaultStyling = {
555
+ theme: "light",
556
+ primaryColor: "#3b82f6",
557
+ backgroundColor: "#ffffff",
558
+ textColor: "#1f2937",
559
+ borderRadius: "md",
560
+ fontSize: "md",
561
+ buttonStyle: "filled",
562
+ labelPosition: "top"
563
+ };
564
+ function getBorderRadius(radius) {
565
+ switch (radius) {
566
+ case "none":
567
+ return "0";
568
+ case "sm":
569
+ return "0.125rem";
570
+ case "md":
571
+ return "0.375rem";
572
+ case "lg":
573
+ return "0.5rem";
574
+ default:
575
+ return "0.375rem";
576
+ }
577
+ }
578
+ function getFontSize(size) {
579
+ switch (size) {
580
+ case "sm":
581
+ return "0.875rem";
582
+ case "md":
583
+ return "1rem";
584
+ case "lg":
585
+ return "1.125rem";
586
+ default:
587
+ return "1rem";
588
+ }
589
+ }
590
+ function FormsExpertForm({
591
+ config,
592
+ slug,
593
+ trackViews,
594
+ submitText = "Submit",
595
+ onSuccess,
596
+ onError,
597
+ onValidationError,
598
+ className,
599
+ style
600
+ }) {
601
+ const form = useForm({
602
+ slug,
603
+ config,
604
+ trackViews,
605
+ onSuccess,
606
+ onError,
607
+ onValidationError,
608
+ autoInit: true
609
+ });
610
+ const [captchaToken, setCaptchaToken] = (0, import_react2.useState)(null);
611
+ const captchaContainerRef = (0, import_react2.useRef)(null);
612
+ const captchaWidgetId = (0, import_react2.useRef)(null);
613
+ const captchaScriptLoaded = (0, import_react2.useRef)(false);
614
+ (0, import_react2.useEffect)(() => {
615
+ if (!form.requiresCaptcha || !form.captchaSiteKey || !form.captchaProvider) return;
616
+ if (captchaScriptLoaded.current) return;
617
+ const provider = form.captchaProvider;
618
+ const siteKey = form.captchaSiteKey;
619
+ const renderWidget = () => {
620
+ if (!captchaContainerRef.current) return;
621
+ const w2 = window;
622
+ if (provider === "turnstile" && w2.turnstile) {
623
+ captchaWidgetId.current = w2.turnstile.render(captchaContainerRef.current, {
624
+ sitekey: siteKey,
625
+ callback: (token) => setCaptchaToken(token),
626
+ "expired-callback": () => setCaptchaToken(null),
627
+ "error-callback": () => setCaptchaToken(null)
628
+ });
629
+ } else if (provider === "hcaptcha" && w2.hcaptcha) {
630
+ captchaWidgetId.current = w2.hcaptcha.render(captchaContainerRef.current, {
631
+ sitekey: siteKey,
632
+ callback: (token) => setCaptchaToken(token),
633
+ "expired-callback": () => setCaptchaToken(null),
634
+ "error-callback": () => setCaptchaToken(null)
635
+ });
636
+ } else if (provider === "recaptcha" && w2.grecaptcha) {
637
+ w2.grecaptcha.ready(() => {
638
+ w2.grecaptcha.execute(siteKey, { action: "submit" }).then((token) => {
639
+ setCaptchaToken(token);
640
+ });
641
+ });
642
+ }
643
+ };
644
+ const w = window;
645
+ if (provider === "turnstile" && w.turnstile || provider === "hcaptcha" && w.hcaptcha || provider === "recaptcha" && w.grecaptcha) {
646
+ captchaScriptLoaded.current = true;
647
+ renderWidget();
648
+ return;
649
+ }
650
+ const script = document.createElement("script");
651
+ if (provider === "turnstile") {
652
+ script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
653
+ } else if (provider === "hcaptcha") {
654
+ script.src = "https://js.hcaptcha.com/1/api.js?render=explicit";
655
+ } else if (provider === "recaptcha") {
656
+ script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`;
657
+ }
658
+ script.async = true;
659
+ script.defer = true;
660
+ script.onload = () => {
661
+ captchaScriptLoaded.current = true;
662
+ setTimeout(renderWidget, 100);
663
+ };
664
+ document.head.appendChild(script);
665
+ return () => {
666
+ if (captchaWidgetId.current !== null) {
667
+ const w2 = window;
668
+ if (provider === "turnstile" && w2.turnstile) {
669
+ w2.turnstile.remove(captchaWidgetId.current);
670
+ } else if (provider === "hcaptcha" && w2.hcaptcha) {
671
+ w2.hcaptcha.reset(captchaWidgetId.current);
672
+ }
673
+ }
674
+ };
675
+ }, [form.requiresCaptcha, form.captchaProvider, form.captchaSiteKey]);
676
+ const resetCaptcha = () => {
677
+ setCaptchaToken(null);
678
+ if (captchaWidgetId.current !== null) {
679
+ const w = window;
680
+ if (form.captchaProvider === "turnstile" && w.turnstile) {
681
+ w.turnstile.reset(captchaWidgetId.current);
682
+ } else if (form.captchaProvider === "hcaptcha" && w.hcaptcha) {
683
+ w.hcaptcha.reset(captchaWidgetId.current);
684
+ } else if (form.captchaProvider === "recaptcha" && w.grecaptcha) {
685
+ w.grecaptcha.execute(form.captchaSiteKey, { action: "submit" }).then((token) => {
686
+ setCaptchaToken(token);
687
+ });
688
+ }
689
+ }
690
+ };
691
+ const styling = form.config?.schema?.styling || defaultStyling;
692
+ const radius = getBorderRadius(styling.borderRadius);
693
+ const fontSize = getFontSize(styling.fontSize);
694
+ const handleSubmit = async (e) => {
695
+ e.preventDefault();
696
+ const result = await form.submit(captchaToken || void 0);
697
+ if (!result) {
698
+ resetCaptcha();
699
+ }
700
+ };
701
+ const handleChange = (e) => {
702
+ const { name, value, type } = e.target;
703
+ if (type === "checkbox") {
704
+ form.setValue(name, e.target.checked);
705
+ } else if (type === "file") {
706
+ form.setValue(name, e.target.files?.[0]);
707
+ } else {
708
+ form.setValue(name, value);
709
+ }
710
+ };
711
+ if (form.isInitializing) {
712
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
713
+ "div",
714
+ {
715
+ className,
716
+ style: {
717
+ padding: "2rem",
718
+ textAlign: "center",
719
+ color: styling.textColor,
720
+ ...style
721
+ },
722
+ children: "Loading form..."
723
+ }
724
+ );
725
+ }
726
+ if (!form.config?.active) {
727
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
728
+ "div",
729
+ {
730
+ className,
731
+ style: {
732
+ padding: "2rem",
733
+ textAlign: "center",
734
+ color: "#ef4444",
735
+ ...style
736
+ },
737
+ children: "This form is not available"
738
+ }
739
+ );
740
+ }
741
+ if (form.isSubmitted) {
742
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
743
+ "div",
744
+ {
745
+ className,
746
+ style: {
747
+ padding: "2rem",
748
+ textAlign: "center",
749
+ backgroundColor: styling.backgroundColor,
750
+ color: styling.textColor,
751
+ borderRadius: radius,
752
+ ...style
753
+ },
754
+ children: [
755
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
756
+ "svg",
757
+ {
758
+ style: {
759
+ width: "3rem",
760
+ height: "3rem",
761
+ margin: "0 auto 1rem",
762
+ color: "#22c55e"
763
+ },
764
+ fill: "none",
765
+ stroke: "currentColor",
766
+ viewBox: "0 0 24 24",
767
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
768
+ "path",
769
+ {
770
+ strokeLinecap: "round",
771
+ strokeLinejoin: "round",
772
+ strokeWidth: 2,
773
+ d: "M5 13l4 4L19 7"
774
+ }
775
+ )
776
+ }
777
+ ),
778
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { fontSize: "1.125rem", fontWeight: 500 }, children: form.config.settings?.successMessage || "Form submitted successfully!" }),
779
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
780
+ "button",
781
+ {
782
+ type: "button",
783
+ onClick: form.reset,
784
+ style: {
785
+ marginTop: "1rem",
786
+ padding: "0.5rem 1rem",
787
+ backgroundColor: "transparent",
788
+ color: styling.primaryColor,
789
+ border: `1px solid ${styling.primaryColor}`,
790
+ borderRadius: radius,
791
+ cursor: "pointer"
792
+ },
793
+ children: "Submit another response"
794
+ }
795
+ )
796
+ ]
797
+ }
798
+ );
799
+ }
800
+ const fields = form.config.schema?.fields || [];
801
+ const showBranding = form.config.branding?.enabled !== false;
802
+ const brandingText = form.config.branding?.text || "Powered by Mira";
803
+ const brandingUrl = form.config.branding?.url || "https://mira.io";
804
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
805
+ "form",
806
+ {
807
+ onSubmit: handleSubmit,
808
+ className,
809
+ style: {
810
+ fontFamily: "system-ui, -apple-system, sans-serif",
811
+ fontSize,
812
+ backgroundColor: styling.backgroundColor,
813
+ color: styling.textColor,
814
+ padding: "1.5rem",
815
+ borderRadius: radius,
816
+ ...style
817
+ },
818
+ children: [
819
+ fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
820
+ FormFieldInput,
821
+ {
822
+ field,
823
+ value: form.values[field.name],
824
+ error: form.errors[field.name],
825
+ onChange: handleChange,
826
+ styling
827
+ },
828
+ field.name
829
+ )),
830
+ form.honeypotEnabled && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { position: "absolute", left: "-9999px", opacity: 0, height: 0, overflow: "hidden" }, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "text", name: "_hp", tabIndex: -1, autoComplete: "off" }) }),
831
+ form.requiresCaptcha && form.captchaProvider !== "recaptcha" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: captchaContainerRef, style: { marginTop: "1rem" } }),
832
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
833
+ "button",
834
+ {
835
+ type: "submit",
836
+ disabled: form.isLoading,
837
+ style: {
838
+ width: "100%",
839
+ padding: "0.625rem 1.25rem",
840
+ marginTop: "1rem",
841
+ fontWeight: 500,
842
+ fontSize,
843
+ fontFamily: "inherit",
844
+ borderRadius: radius,
845
+ cursor: form.isLoading ? "not-allowed" : "pointer",
846
+ opacity: form.isLoading ? 0.5 : 1,
847
+ backgroundColor: styling.buttonStyle === "filled" ? styling.primaryColor : "transparent",
848
+ color: styling.buttonStyle === "filled" ? "white" : styling.primaryColor,
849
+ border: styling.buttonStyle === "filled" ? "none" : `2px solid ${styling.primaryColor}`
850
+ },
851
+ children: form.isLoading ? "Submitting..." : submitText
852
+ }
853
+ ),
854
+ showBranding && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
855
+ "div",
856
+ {
857
+ style: {
858
+ textAlign: "center",
859
+ marginTop: "1rem",
860
+ paddingTop: "0.75rem",
861
+ borderTop: `1px solid ${styling.theme === "dark" ? "#374151" : "#e5e7eb"}`
862
+ },
863
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
864
+ "a",
865
+ {
866
+ href: brandingUrl,
867
+ target: "_blank",
868
+ rel: "noopener noreferrer",
869
+ style: {
870
+ color: styling.theme === "dark" ? "#9ca3af" : "#6b7280",
871
+ textDecoration: "none",
872
+ fontSize: "0.75rem"
873
+ },
874
+ children: brandingText
875
+ }
876
+ )
877
+ }
878
+ )
879
+ ]
880
+ }
881
+ );
882
+ }
883
+ function FormFieldInput({
884
+ field,
885
+ value,
886
+ error,
887
+ onChange,
888
+ styling
889
+ }) {
890
+ const radius = getBorderRadius(styling.borderRadius);
891
+ const fontSize = getFontSize(styling.fontSize);
892
+ const inputStyle = {
893
+ width: "100%",
894
+ padding: "0.5rem 0.75rem",
895
+ border: `1px solid ${error ? "#ef4444" : styling.theme === "dark" ? "#4b5563" : "#d1d5db"}`,
896
+ borderRadius: radius,
897
+ fontSize,
898
+ fontFamily: "inherit",
899
+ backgroundColor: styling.theme === "dark" ? "#374151" : "#ffffff",
900
+ color: styling.textColor
901
+ };
902
+ if (field.type === "checkbox") {
903
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "1rem" }, children: [
904
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
905
+ "input",
906
+ {
907
+ type: "checkbox",
908
+ id: field.name,
909
+ name: field.name,
910
+ checked: Boolean(value),
911
+ onChange,
912
+ required: field.required,
913
+ style: { width: "1rem", height: "1rem", accentColor: styling.primaryColor }
914
+ }
915
+ ),
916
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { htmlFor: field.name, children: [
917
+ field.label || field.name,
918
+ field.required && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: "#ef4444", marginLeft: "0.25rem" }, children: "*" })
919
+ ] }),
920
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: "#ef4444", fontSize: "0.875rem", marginLeft: "0.5rem" }, children: error })
921
+ ] });
922
+ }
923
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
924
+ "div",
925
+ {
926
+ style: {
927
+ marginBottom: "1rem",
928
+ ...styling.labelPosition === "left" ? { display: "flex", alignItems: "flex-start", gap: "1rem" } : {}
929
+ },
930
+ children: [
931
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
932
+ "label",
933
+ {
934
+ htmlFor: field.name,
935
+ style: {
936
+ display: "block",
937
+ fontWeight: 500,
938
+ ...styling.labelPosition === "left" ? { width: "33%", flexShrink: 0, paddingTop: "0.5rem" } : { marginBottom: "0.25rem" }
939
+ },
940
+ children: [
941
+ field.label || field.name,
942
+ field.required && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: "#ef4444", marginLeft: "0.25rem" }, children: "*" })
943
+ ]
944
+ }
945
+ ),
946
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styling.labelPosition === "left" ? { flex: 1 } : {}, children: [
947
+ field.type === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
948
+ "textarea",
949
+ {
950
+ id: field.name,
951
+ name: field.name,
952
+ value: String(value || ""),
953
+ onChange,
954
+ placeholder: field.placeholder,
955
+ required: field.required,
956
+ style: { ...inputStyle, minHeight: "100px", resize: "vertical" }
957
+ }
958
+ ) : field.type === "select" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
959
+ "select",
960
+ {
961
+ id: field.name,
962
+ name: field.name,
963
+ value: String(value || ""),
964
+ onChange,
965
+ required: field.required,
966
+ style: inputStyle,
967
+ children: [
968
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "", children: "Select an option..." }),
969
+ field.options?.map((opt) => {
970
+ const val = typeof opt === "string" ? opt : opt.value;
971
+ const label = typeof opt === "string" ? opt : opt.label;
972
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: val, children: label }, val);
973
+ })
974
+ ]
975
+ }
976
+ ) : field.type === "file" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
977
+ "input",
978
+ {
979
+ type: "file",
980
+ id: field.name,
981
+ name: field.name,
982
+ onChange,
983
+ required: field.required,
984
+ accept: field.allowedMimeTypes?.join(","),
985
+ multiple: field.multiple,
986
+ style: inputStyle
987
+ }
988
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
989
+ "input",
990
+ {
991
+ type: field.type,
992
+ id: field.name,
993
+ name: field.name,
994
+ value: String(value || ""),
995
+ onChange,
996
+ placeholder: field.placeholder,
997
+ required: field.required,
998
+ style: inputStyle
999
+ }
1000
+ ),
1001
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { color: "#ef4444", fontSize: "0.875rem", marginTop: "0.25rem" }, children: error })
1002
+ ] })
1003
+ ]
1004
+ }
1005
+ );
1006
+ }
1007
+ // Annotate the CommonJS export names for ESM import in node:
1008
+ 0 && (module.exports = {
1009
+ FormsExpertForm,
1010
+ FormsProvider,
1011
+ useForm,
1012
+ useFormsSDK
1013
+ });
1014
+ //# sourceMappingURL=index.cjs.map