@formsnip/embed 1.1.1 → 1.1.2

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.
package/dist/index.js CHANGED
@@ -2,13 +2,22 @@
2
2
 
3
3
  var React = require('react');
4
4
 
5
- const ALLOWED_FILE_EXTENSIONS = '.jpg,.jpeg,.png,.gif,.webp,.svg,.pdf,.mp4,.mpeg,.mov,.avi,.webm,.mkv,.mp3,.wav,.ogg,.aac,.flac';
5
+ const HONEYPOT_FIELD = '_fs_hp';
6
+ const SUBMIT_TOKEN_FIELD = '_fs_token';
7
+
8
+ const ALLOWED_FILE_EXTENSIONS = '.jpg,.jpeg,.png,.gif,.webp,.pdf,.mp4,.mpeg,.mov,.avi,.webm,.mkv,.mp3,.wav,.ogg,.aac,.flac';
6
9
  const MAX_TOTAL_FILE_SIZE = 15 * 1024 * 1024; // 15MB
7
10
  /**
8
11
  * Upload a file to R2 via presigned URL
9
12
  */
10
- async function uploadFile(file, formId, baseUrl) {
13
+ async function uploadFile(file, formId, baseUrl, submitToken) {
11
14
  try {
15
+ if (!submitToken) {
16
+ return {
17
+ success: false,
18
+ error: 'Form session expired. Please reload and try again.',
19
+ };
20
+ }
12
21
  // Step 1: Get presigned upload URL
13
22
  const signResponse = await fetch(`${baseUrl}/uploads/sign`, {
14
23
  method: 'POST',
@@ -21,6 +30,7 @@ async function uploadFile(file, formId, baseUrl) {
21
30
  contentType: file.type,
22
31
  size: file.size,
23
32
  formId,
33
+ [SUBMIT_TOKEN_FIELD]: submitToken,
24
34
  }),
25
35
  });
26
36
  if (!signResponse.ok) {
@@ -66,7 +76,7 @@ async function uploadFile(file, formId, baseUrl) {
66
76
  /**
67
77
  * Upload multiple files to R2
68
78
  */
69
- async function uploadFiles(files, formId, baseUrl) {
79
+ async function uploadFiles(files, formId, baseUrl, submitToken) {
70
80
  // Check total size
71
81
  const totalSize = files.reduce((sum, file) => sum + file.size, 0);
72
82
  if (totalSize > MAX_TOTAL_FILE_SIZE) {
@@ -75,7 +85,7 @@ async function uploadFiles(files, formId, baseUrl) {
75
85
  errors: [`Total file size exceeds 15MB limit per submission`],
76
86
  };
77
87
  }
78
- const results = await Promise.allSettled(files.map((file) => uploadFile(file, formId, baseUrl)));
88
+ const results = await Promise.allSettled(files.map((file) => uploadFile(file, formId, baseUrl, submitToken)));
79
89
  const fileKeys = [];
80
90
  const errors = [];
81
91
  results.forEach((result, index) => {
@@ -263,6 +273,7 @@ function injectBaseStyles() {
263
273
  '.fs-space-x-2>*+*{margin-left:0.5rem}',
264
274
  '.fs-outline-none{outline:none}',
265
275
  '.fs-hover-opacity:hover{opacity:0.9}',
276
+ '.formsnip-embed-ph::placeholder{color:var(--fs-placeholder-color,#9ca3af);opacity:1}',
266
277
  ].join('');
267
278
  const style = document.createElement('style');
268
279
  style.id = 'formsnip-base-styles';
@@ -298,6 +309,9 @@ function applyStyles(styles, customStyles) {
298
309
  if (customStyles.fontSize !== undefined) {
299
310
  result.fontSize = `${customStyles.fontSize}px`;
300
311
  }
312
+ if (customStyles.placeholderColor) {
313
+ result['--fs-placeholder-color'] = customStyles.placeholderColor;
314
+ }
301
315
  // Note: fullWidth is handled separately in the component, not here
302
316
  // to ensure proper display property handling
303
317
  return result;
@@ -335,6 +349,86 @@ function pushFormSnipSubmissionDataLayer(options) {
335
349
  }
336
350
  }
337
351
 
352
+ const PHONE_COUNTRIES = [
353
+ { iso: 'AU', name: 'Australia', dial: '+61' },
354
+ { iso: 'AT', name: 'Austria', dial: '+43' },
355
+ { iso: 'BE', name: 'Belgium', dial: '+32' },
356
+ { iso: 'BR', name: 'Brazil', dial: '+55' },
357
+ { iso: 'CA', name: 'Canada', dial: '+1' },
358
+ { iso: 'CN', name: 'China', dial: '+86' },
359
+ { iso: 'DK', name: 'Denmark', dial: '+45' },
360
+ { iso: 'FI', name: 'Finland', dial: '+358' },
361
+ { iso: 'FR', name: 'France', dial: '+33' },
362
+ { iso: 'DE', name: 'Germany', dial: '+49' },
363
+ { iso: 'GR', name: 'Greece', dial: '+30' },
364
+ { iso: 'HK', name: 'Hong Kong', dial: '+852' },
365
+ { iso: 'IS', name: 'Iceland', dial: '+354' },
366
+ { iso: 'IN', name: 'India', dial: '+91' },
367
+ { iso: 'IE', name: 'Ireland', dial: '+353' },
368
+ { iso: 'IT', name: 'Italy', dial: '+39' },
369
+ { iso: 'JP', name: 'Japan', dial: '+81' },
370
+ { iso: 'LU', name: 'Luxembourg', dial: '+352' },
371
+ { iso: 'MX', name: 'Mexico', dial: '+52' },
372
+ { iso: 'NL', name: 'Netherlands', dial: '+31' },
373
+ { iso: 'NZ', name: 'New Zealand', dial: '+64' },
374
+ { iso: 'NO', name: 'Norway', dial: '+47' },
375
+ { iso: 'PL', name: 'Poland', dial: '+48' },
376
+ { iso: 'PT', name: 'Portugal', dial: '+351' },
377
+ { iso: 'SG', name: 'Singapore', dial: '+65' },
378
+ { iso: 'ZA', name: 'South Africa', dial: '+27' },
379
+ { iso: 'KR', name: 'South Korea', dial: '+82' },
380
+ { iso: 'ES', name: 'Spain', dial: '+34' },
381
+ { iso: 'SE', name: 'Sweden', dial: '+46' },
382
+ { iso: 'CH', name: 'Switzerland', dial: '+41' },
383
+ { iso: 'AE', name: 'United Arab Emirates', dial: '+971' },
384
+ { iso: 'GB', name: 'United Kingdom', dial: '+44' },
385
+ { iso: 'US', name: 'United States', dial: '+1' },
386
+ ];
387
+ const PREFERRED_ISO_FOR_DIAL = {
388
+ '+1': 'US',
389
+ '+44': 'GB',
390
+ };
391
+ function defaultPhoneIso() {
392
+ try {
393
+ const region = new Intl.Locale(navigator.language).region;
394
+ if (region && PHONE_COUNTRIES.some((country) => country.iso === region)) {
395
+ return region;
396
+ }
397
+ }
398
+ catch {
399
+ /* ignore */
400
+ }
401
+ return 'DK';
402
+ }
403
+ function phoneCountryByIso(iso) {
404
+ return PHONE_COUNTRIES.find((country) => country.iso === iso) ?? PHONE_COUNTRIES.find((country) => country.iso === 'DK');
405
+ }
406
+ function formatPhoneValue(dial, national) {
407
+ const digits = national.replace(/[^\d\s()-]/g, '').trim();
408
+ if (!digits)
409
+ return '';
410
+ return `${dial} ${digits}`;
411
+ }
412
+ function parsePhoneValue(value, fallbackIso = 'DK') {
413
+ const trimmed = value.trim();
414
+ if (!trimmed.startsWith('+')) {
415
+ return { iso: fallbackIso, national: trimmed };
416
+ }
417
+ const match = trimmed.match(/^(\+\d{1,4})(?:\s+(.*))?$/);
418
+ if (!match) {
419
+ return { iso: fallbackIso, national: trimmed };
420
+ }
421
+ const dial = match[1];
422
+ const national = (match[2] ?? '').trim();
423
+ const matches = PHONE_COUNTRIES.filter((country) => country.dial === dial);
424
+ if (matches.length === 0) {
425
+ return { iso: fallbackIso, national: trimmed.replace(/^\+\d{1,4}\s*/, '') };
426
+ }
427
+ const preferred = PREFERRED_ISO_FOR_DIAL[dial];
428
+ const country = matches.find((item) => item.iso === preferred) ?? matches[0];
429
+ return { iso: country.iso, national };
430
+ }
431
+
338
432
  /** Build rows from flat fields (full = own row, half = paired in row) */
339
433
  function buildRowsFromFields(fields) {
340
434
  const rows = [];
@@ -370,6 +464,8 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
370
464
  const [submitError, setSubmitError] = React.useState(null);
371
465
  const [submitSuccess, setSubmitSuccess] = React.useState(false);
372
466
  const [isSubmitting, setIsSubmitting] = React.useState(false);
467
+ const [submitToken, setSubmitToken] = React.useState('');
468
+ const [honeypot, setHoneypot] = React.useState('');
373
469
  React.useEffect(() => {
374
470
  // Inject base styles on mount (same as embed script)
375
471
  injectBaseStyles();
@@ -384,6 +480,7 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
384
480
  // Form data includes embedStyles from the database
385
481
  // These styles are set in the EmbedForm page and stored in the Form model
386
482
  setForm(json.data);
483
+ setSubmitToken(json.data.submitToken || '');
387
484
  // Parse URL query once for prefill support
388
485
  let urlParams = null;
389
486
  if (typeof window !== 'undefined') {
@@ -396,22 +493,27 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
396
493
  return acc;
397
494
  }
398
495
  let initialValue = '';
399
- const prefillKey = field.urlPrefillParam;
496
+ const prefillKey = field.id;
400
497
  const canPrefillType = field.type === 'text' ||
401
498
  field.type === 'email' ||
402
499
  field.type === 'textarea' ||
403
500
  field.type === 'number' ||
404
501
  field.type === 'date' ||
405
502
  field.type === 'datetime' ||
503
+ field.type === 'time' ||
406
504
  field.type === 'phone' ||
505
+ field.type === 'website' ||
506
+ field.type === 'choice' ||
407
507
  field.type === 'hidden' ||
408
508
  field.type === 'select';
409
509
  // 1) Prefill prop (takes precedence over URL)
410
510
  if (prefill && prefillKey && prefillKey.trim() !== '' && prefillKey in prefill) {
411
511
  const v = prefill[prefillKey];
412
512
  if (typeof v === 'string' && canPrefillType) {
413
- if (field.type === 'select' && Array.isArray(field.options) && !field.options.includes(v)) {
414
- // Ignore invalid option values for selects
513
+ const options = field.options || [];
514
+ const optionField = field.type === 'select' || field.type === 'choice';
515
+ if (optionField && options.length > 0 && !options.includes(v)) {
516
+ // Ignore invalid option values
415
517
  }
416
518
  else {
417
519
  initialValue = v;
@@ -423,8 +525,10 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
423
525
  const v = urlParams.get(prefillKey);
424
526
  if (v !== null && v !== '') {
425
527
  if (canPrefillType) {
426
- if (field.type === 'select' && Array.isArray(field.options) && !field.options.includes(v)) {
427
- // Ignore invalid option values for selects
528
+ const options = field.options || [];
529
+ const optionField = field.type === 'select' || field.type === 'choice';
530
+ if (optionField && options.length > 0 && !options.includes(v)) {
531
+ // Ignore invalid option values
428
532
  }
429
533
  else {
430
534
  initialValue = v;
@@ -477,7 +581,7 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
477
581
  delete newErrors[fieldId];
478
582
  return newErrors;
479
583
  });
480
- const result = await uploadFiles(files, formId, baseUrl);
584
+ const result = await uploadFiles(files, formId, baseUrl, submitToken);
481
585
  setUploadingFiles((prev) => ({ ...prev, [fieldId]: false }));
482
586
  if (result.success && result.fileKeys && result.fileKeys.length > 0) {
483
587
  const existingFileKeys = Array.isArray(formData[fieldId])
@@ -501,6 +605,16 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
501
605
  setSubmitSuccess(false);
502
606
  setIsSubmitting(true);
503
607
  try {
608
+ const pageUrl = new URL(window.location.href);
609
+ const payload = {
610
+ ...formData,
611
+ [SUBMIT_TOKEN_FIELD]: submitToken,
612
+ [HONEYPOT_FIELD]: honeypot,
613
+ _fs_page: pageUrl.href,
614
+ _fs_utm_source: pageUrl.searchParams.get('utm_source') || undefined,
615
+ _fs_utm_medium: pageUrl.searchParams.get('utm_medium') || undefined,
616
+ _fs_utm_campaign: pageUrl.searchParams.get('utm_campaign') || undefined,
617
+ };
504
618
  const response = await fetch(`${baseUrl}/submissions/create-submission/${formId}`, {
505
619
  method: 'POST',
506
620
  headers: {
@@ -508,7 +622,7 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
508
622
  'X-Requested-With': 'XMLHttpRequest',
509
623
  },
510
624
  credentials: 'include',
511
- body: JSON.stringify(formData),
625
+ body: JSON.stringify(payload),
512
626
  });
513
627
  const json = await response.json();
514
628
  if (response.ok && (json.success === undefined || json.success === true)) {
@@ -520,6 +634,11 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
520
634
  if (onSuccess) {
521
635
  onSuccess(json.data);
522
636
  }
637
+ const redirectUrl = form?.validation?.redirectUrl?.trim();
638
+ if (redirectUrl && /^https?:\/\//i.test(redirectUrl)) {
639
+ window.location.assign(redirectUrl);
640
+ return;
641
+ }
523
642
  // Reset form
524
643
  const emptyData = Object.keys(formData).reduce((acc, key) => {
525
644
  const field = form?.fields.find((f) => f.id === key);
@@ -529,6 +648,15 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
529
648
  setFormData(emptyData);
530
649
  setSelectedFiles({});
531
650
  setUploadErrors({});
651
+ setHoneypot('');
652
+ try {
653
+ const refresh = await fetch(`${baseUrl}/forms/get-form/${formId}`);
654
+ const refreshJson = await refresh.json();
655
+ if (refreshJson?.data?.submitToken) {
656
+ setSubmitToken(refreshJson.data.submitToken);
657
+ }
658
+ }
659
+ catch (_) { }
532
660
  }
533
661
  else {
534
662
  const errorMsg = json.message || 'Submission failed';
@@ -553,6 +681,7 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
553
681
  // Use embedStyles from database (set in EmbedForm page)
554
682
  const fieldStyles = form?.embedStyles?.field || {};
555
683
  const isRequired = field.required === true;
684
+ const inputStyle = applyStyles({}, fieldStyles);
556
685
  const commonProps = {
557
686
  id: `field-${field.id}`,
558
687
  name: field.id,
@@ -560,8 +689,8 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
560
689
  onChange: (e) => handleInputChange(field.id, e.target.value),
561
690
  placeholder: field.placeholder || '',
562
691
  required: isRequired,
563
- style: applyStyles({}, fieldStyles),
564
- className: field.type === 'textarea' ? CLASSES.textarea : CLASSES.input,
692
+ style: inputStyle,
693
+ className: `${field.type === 'textarea' ? CLASSES.textarea : CLASSES.input} formsnip-embed-ph`,
565
694
  };
566
695
  const labelStyle = {};
567
696
  if (fieldStyles.labelFontSize) {
@@ -570,18 +699,54 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
570
699
  if (field.type === 'hidden') {
571
700
  return (React.createElement("input", { type: "hidden", id: `field-${field.id}`, name: field.id, value: formData[field.id] || field.value || '' }));
572
701
  }
702
+ if (field.type === 'heading') {
703
+ return React.createElement("h2", { style: { margin: 0, fontSize: '1.5rem', fontWeight: 600 } }, field.label);
704
+ }
705
+ if (field.type === 'paragraph') {
706
+ return React.createElement("p", { style: { margin: 0, whiteSpace: 'pre-wrap', color: '#4b5563' } }, field.content || '');
707
+ }
708
+ if (field.type === 'divider') {
709
+ return React.createElement("hr", { style: { border: 0, borderTop: '1px solid #e5e7eb', margin: '0.25rem 0' } });
710
+ }
711
+ if (field.type === 'choice') {
712
+ const options = field.options && field.options.length > 0 ? field.options : ['Yes', 'No'];
713
+ return (React.createElement("div", { className: "fs-relative" },
714
+ !field.hideLabel && (React.createElement("label", { className: "fs-mb-2 fs-flex fs-w-full fs-items-center fs-justify-between fs-gap-2 fs-text-sm fs-text-gray-700", style: { fontWeight: 500, ...labelStyle } },
715
+ React.createElement("span", { className: "fs-min-w-0 fs-flex-1" }, field.label),
716
+ isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0", style: { lineHeight: 1 } }, "*"))),
717
+ options.map((option) => (React.createElement("label", { key: option, className: "fs-flex fs-items-center", style: { marginTop: '0.4rem' } },
718
+ React.createElement("input", { type: "radio", name: field.id, value: option, checked: formData[field.id] === option, onChange: (event) => handleInputChange(field.id, event.target.value), required: isRequired }),
719
+ React.createElement("span", { style: { marginLeft: '0.5rem' } }, option))))));
720
+ }
721
+ if (field.type === 'phone') {
722
+ const parsed = parsePhoneValue(formData[field.id] || '', defaultPhoneIso());
723
+ const country = phoneCountryByIso(parsed.iso);
724
+ return (React.createElement(React.Fragment, null,
725
+ !field.hideLabel && (React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-mb-2 fs-flex fs-w-full fs-items-center fs-justify-between fs-gap-2 fs-text-sm fs-text-gray-700", style: { fontWeight: 500, ...labelStyle } },
726
+ React.createElement("span", { className: "fs-min-w-0 fs-flex-1" }, field.label),
727
+ isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0", style: { lineHeight: 1 } }, "*"))),
728
+ React.createElement("div", { style: { display: 'flex', gap: '0.5rem' } },
729
+ React.createElement("select", { "aria-label": "Country code", value: country.iso, onChange: (event) => {
730
+ const next = phoneCountryByIso(event.target.value);
731
+ handleInputChange(field.id, formatPhoneValue(next.dial, parsed.national));
732
+ }, className: CLASSES.input, style: { ...inputStyle, width: '8.5rem', flex: '0 0 auto' } }, PHONE_COUNTRIES.map((item) => (React.createElement("option", { key: item.iso, value: item.iso },
733
+ item.dial,
734
+ " ",
735
+ item.name)))),
736
+ React.createElement("input", { id: `field-${field.id}`, name: field.id, type: "tel", value: parsed.national, onChange: (event) => handleInputChange(field.id, formatPhoneValue(country.dial, event.target.value)), placeholder: field.placeholder || '12 34 56 78', required: isRequired, style: { ...inputStyle, flex: 1 }, className: `${CLASSES.input} formsnip-embed-ph` }))));
737
+ }
573
738
  // For checkbox and radio, return without extra wrapper
574
739
  if (field.type === 'checkbox') {
575
- return (React.createElement("label", { className: "fs-flex fs-items-center" },
740
+ return (React.createElement("label", { className: "fs-flex fs-w-full fs-items-center fs-gap-2" },
576
741
  React.createElement("input", { type: "checkbox", id: `field-${field.id}`, name: field.id, checked: String(formData[field.id]) === 'true' || formData[field.id] === 'on', onChange: (e) => handleInputChange(field.id, e.target.checked ? 'true' : 'false'), required: isRequired }),
577
- React.createElement("span", { style: { marginLeft: '0.5rem' } }, field.label || field.placeholder || 'Check this box'),
578
- isRequired && React.createElement("span", { className: "fs-text-red-500", style: { marginLeft: '0.25rem' } }, "*")));
742
+ !field.hideLabel && (React.createElement("span", { className: "fs-min-w-0 fs-flex-1", style: { marginLeft: '0.5rem' } }, field.label || field.placeholder || 'Check this box')),
743
+ isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0 fs-ml-auto", style: { lineHeight: 1 } }, "*")));
579
744
  }
580
745
  if (field.type === 'radio') {
581
746
  return (React.createElement("div", { className: "fs-relative" },
582
- React.createElement("label", { className: "fs-text-sm fs-text-gray-700 fs-mb-2", style: { fontWeight: 500 } },
583
- field.label,
584
- isRequired && React.createElement("span", { className: "fs-text-red-500", style: { marginLeft: '0.25rem' } }, "*")),
747
+ !field.hideLabel && (React.createElement("label", { className: "fs-mb-2 fs-flex fs-w-full fs-items-center fs-justify-between fs-gap-2 fs-text-sm fs-text-gray-700", style: { fontWeight: 500 } },
748
+ React.createElement("span", { className: "fs-min-w-0 fs-flex-1" }, field.label),
749
+ isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0", style: { lineHeight: 1 } }, "*"))),
585
750
  React.createElement("label", { className: "fs-flex fs-items-center" },
586
751
  React.createElement("input", { type: "radio", id: `field-${field.id}-yes`, name: field.id, value: "yes", checked: formData[field.id] === 'yes', onChange: (e) => handleInputChange(field.id, e.target.value), required: isRequired }),
587
752
  React.createElement("span", { style: { marginLeft: '0.5rem' } }, " Yes")),
@@ -596,19 +761,19 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
596
761
  ? [formData[field.id]]
597
762
  : [];
598
763
  return (React.createElement(React.Fragment, null,
599
- React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-text-sm fs-text-gray-700 fs-mb-2", style: { fontWeight: 500 } },
600
- field.label,
601
- isRequired && React.createElement("span", { className: "fs-text-red-500", style: { marginLeft: '0.25rem' } }, "*")),
764
+ !field.hideLabel && (React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-mb-2 fs-flex fs-w-full fs-items-center fs-justify-between fs-gap-2 fs-text-sm fs-text-gray-700", style: { fontWeight: 500 } },
765
+ React.createElement("span", { className: "fs-min-w-0 fs-flex-1" }, field.label),
766
+ isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0", style: { lineHeight: 1 } }, "*"))),
602
767
  React.createElement(FileUpload, { id: field.id, required: isRequired, disabled: uploadingFiles[field.id], multiple: true, onFilesChange: (files) => handleFilesChange(field.id, files), uploadedFiles: uploadedFileKeys, uploading: uploadingFiles[field.id], error: uploadErrors[field.id] })));
603
768
  }
604
769
  // For other fields, use minimal wrapper structure
605
770
  return (React.createElement(React.Fragment, null,
606
- React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-text-sm fs-text-gray-700 fs-mb-2", style: { fontWeight: 500, ...labelStyle } },
607
- field.label,
608
- isRequired && React.createElement("span", { className: "fs-text-red-500", style: { marginLeft: '0.25rem' } }, "*")),
609
- React.createElement("div", { className: CLASSES.inner }, field.type === 'select' ? (React.createElement("select", { id: `field-${field.id}`, name: field.id, value: formData[field.id] || '', onChange: (e) => handleInputChange(field.id, e.target.value), required: isRequired, style: applyStyles({}, fieldStyles), className: CLASSES.input },
771
+ !field.hideLabel && (React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-mb-2 fs-flex fs-w-full fs-items-center fs-justify-between fs-gap-2 fs-text-sm fs-text-gray-700", style: { fontWeight: 500, ...labelStyle } },
772
+ React.createElement("span", { className: "fs-min-w-0 fs-flex-1" }, field.label),
773
+ isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0", style: { lineHeight: 1 } }, "*"))),
774
+ React.createElement("div", { className: CLASSES.inner }, field.type === 'select' ? (React.createElement("select", { id: `field-${field.id}`, name: field.id, value: formData[field.id] || '', onChange: (e) => handleInputChange(field.id, e.target.value), required: isRequired, style: inputStyle, className: `${CLASSES.input} formsnip-embed-ph` },
610
775
  React.createElement("option", { value: "" }, field.placeholder || 'Select an option'),
611
- field.options?.map((option, index) => (React.createElement("option", { key: index, value: option }, option))))) : field.type === 'textarea' ? (React.createElement("textarea", { ...commonProps, rows: 4 })) : field.type === 'phone' ? (React.createElement("input", { type: "tel", ...commonProps })) : field.type === 'datetime' ? (React.createElement("input", { type: "datetime-local", ...commonProps })) : (React.createElement("input", { type: field.type || 'text', ...commonProps })))));
776
+ field.options?.map((option, index) => (React.createElement("option", { key: index, value: option }, option))))) : field.type === 'textarea' ? (React.createElement("textarea", { ...commonProps, rows: 4 })) : field.type === 'phone' ? (React.createElement("input", { type: "tel", ...commonProps })) : field.type === 'website' ? (React.createElement("input", { type: "url", ...commonProps })) : field.type === 'time' ? (React.createElement("input", { type: "time", ...commonProps })) : field.type === 'datetime' ? (React.createElement("input", { type: "datetime-local", ...commonProps })) : (React.createElement("input", { type: field.type || 'text', ...commonProps })))));
612
777
  };
613
778
  if (loading) {
614
779
  const content = (React.createElement("div", { className: "fs-text-center", style: { padding: '2rem 0' } },
@@ -637,6 +802,14 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
637
802
  marginBottom: '1rem',
638
803
  } }, form.validation?.confirmationText || 'Thank you! Your submission has been received.')),
639
804
  React.createElement("form", { id: "formsnip-form", onSubmit: handleSubmit, className: CLASSES.form },
805
+ React.createElement("input", { type: "text", name: HONEYPOT_FIELD, value: honeypot, onChange: (e) => setHoneypot(e.target.value), tabIndex: -1, autoComplete: "off", "aria-hidden": "true", style: {
806
+ position: 'absolute',
807
+ left: '-9999px',
808
+ height: 0,
809
+ width: 0,
810
+ opacity: 0,
811
+ overflow: 'hidden',
812
+ } }),
640
813
  form.fields.filter((f) => f.isActive !== false && f.type === 'hidden').map((field) => (React.createElement(React.Fragment, { key: field.id }, renderField(field)))),
641
814
  buildRowsFromFields(form.fields.filter((f) => f.isActive !== false && f.type !== 'hidden')).map((row, rowIdx) => row.length === 1 ? (React.createElement("div", { key: row[0].id, style: { display: 'flex', flexDirection: 'column' } }, renderField(row[0]))) : (React.createElement("div", { key: `row-${rowIdx}`, style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.25rem' } }, row.map((field) => (React.createElement("div", { key: field.id, style: { display: 'flex', flexDirection: 'column' } }, renderField(field))))))),
642
815
  React.createElement("button", { type: "submit", disabled: isSubmitting, className: CLASSES.button, style: {