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