@opensite/ui 1.8.2 → 1.8.4

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 (171) hide show
  1. package/dist/about-story-gallery.cjs +3 -30
  2. package/dist/about-story-gallery.d.cts +1 -1
  3. package/dist/about-story-gallery.d.ts +1 -1
  4. package/dist/about-story-gallery.js +3 -30
  5. package/dist/components.d.cts +1 -1
  6. package/dist/components.d.ts +1 -1
  7. package/dist/contact-callback.cjs +526 -273
  8. package/dist/contact-callback.d.cts +39 -59
  9. package/dist/contact-callback.d.ts +39 -59
  10. package/dist/contact-callback.js +528 -274
  11. package/dist/contact-card.cjs +459 -183
  12. package/dist/contact-card.d.cts +26 -49
  13. package/dist/contact-card.d.ts +26 -49
  14. package/dist/contact-card.js +461 -183
  15. package/dist/contact-careers.cjs +614 -510
  16. package/dist/contact-careers.d.cts +32 -55
  17. package/dist/contact-careers.d.ts +32 -55
  18. package/dist/contact-careers.js +616 -510
  19. package/dist/contact-catering.cjs +507 -501
  20. package/dist/contact-catering.d.cts +27 -61
  21. package/dist/contact-catering.d.ts +27 -61
  22. package/dist/contact-catering.js +509 -500
  23. package/dist/contact-consultation.cjs +484 -253
  24. package/dist/contact-consultation.d.cts +29 -56
  25. package/dist/contact-consultation.d.ts +29 -56
  26. package/dist/contact-consultation.js +486 -253
  27. package/dist/contact-dark.cjs +296 -296
  28. package/dist/contact-dark.d.cts +1 -1
  29. package/dist/contact-dark.d.ts +1 -1
  30. package/dist/contact-dark.js +297 -296
  31. package/dist/contact-demo.d.cts +1 -1
  32. package/dist/contact-demo.d.ts +1 -1
  33. package/dist/contact-emergency.d.cts +1 -1
  34. package/dist/contact-emergency.d.ts +1 -1
  35. package/dist/contact-event.d.cts +1 -1
  36. package/dist/contact-event.d.ts +1 -1
  37. package/dist/contact-faq.cjs +247 -250
  38. package/dist/contact-faq.d.cts +1 -1
  39. package/dist/contact-faq.d.ts +1 -1
  40. package/dist/contact-faq.js +248 -250
  41. package/dist/contact-feedback.d.cts +1 -1
  42. package/dist/contact-feedback.d.ts +1 -1
  43. package/dist/contact-fitness.d.cts +1 -1
  44. package/dist/contact-fitness.d.ts +1 -1
  45. package/dist/contact-guest.d.cts +1 -1
  46. package/dist/contact-guest.d.ts +1 -1
  47. package/dist/contact-image.d.cts +1 -1
  48. package/dist/contact-image.d.ts +1 -1
  49. package/dist/contact-insurance.d.cts +1 -1
  50. package/dist/contact-insurance.d.ts +1 -1
  51. package/dist/contact-interview.d.cts +1 -1
  52. package/dist/contact-interview.d.ts +1 -1
  53. package/dist/contact-locations.d.cts +1 -1
  54. package/dist/contact-locations.d.ts +1 -1
  55. package/dist/contact-maintenance.d.cts +1 -1
  56. package/dist/contact-maintenance.d.ts +1 -1
  57. package/dist/contact-map.d.cts +1 -1
  58. package/dist/contact-map.d.ts +1 -1
  59. package/dist/contact-minimal.d.cts +1 -1
  60. package/dist/contact-minimal.d.ts +1 -1
  61. package/dist/contact-moving.d.cts +1 -1
  62. package/dist/contact-moving.d.ts +1 -1
  63. package/dist/contact-multistep.d.cts +1 -1
  64. package/dist/contact-multistep.d.ts +1 -1
  65. package/dist/contact-partnership.d.cts +1 -1
  66. package/dist/contact-partnership.d.ts +1 -1
  67. package/dist/contact-photography.cjs +247 -250
  68. package/dist/contact-photography.d.cts +1 -1
  69. package/dist/contact-photography.d.ts +1 -1
  70. package/dist/contact-photography.js +248 -250
  71. package/dist/contact-press.d.cts +1 -1
  72. package/dist/contact-press.d.ts +1 -1
  73. package/dist/contact-quote.d.cts +1 -1
  74. package/dist/contact-quote.d.ts +1 -1
  75. package/dist/contact-referral.d.cts +1 -1
  76. package/dist/contact-referral.d.ts +1 -1
  77. package/dist/contact-report.d.cts +1 -1
  78. package/dist/contact-report.d.ts +1 -1
  79. package/dist/contact-reservation.d.cts +1 -1
  80. package/dist/contact-reservation.d.ts +1 -1
  81. package/dist/contact-retreat.d.cts +1 -1
  82. package/dist/contact-retreat.d.ts +1 -1
  83. package/dist/contact-rsvp.d.cts +1 -1
  84. package/dist/contact-rsvp.d.ts +1 -1
  85. package/dist/contact-sales.d.cts +1 -1
  86. package/dist/contact-sales.d.ts +1 -1
  87. package/dist/contact-schedule.d.cts +1 -1
  88. package/dist/contact-schedule.d.ts +1 -1
  89. package/dist/contact-sponsorship.d.cts +1 -1
  90. package/dist/contact-sponsorship.d.ts +1 -1
  91. package/dist/contact-support.d.cts +1 -1
  92. package/dist/contact-support.d.ts +1 -1
  93. package/dist/contact-tenant.d.cts +1 -1
  94. package/dist/contact-tenant.d.ts +1 -1
  95. package/dist/contact-vendor.d.cts +1 -1
  96. package/dist/contact-vendor.d.ts +1 -1
  97. package/dist/contact-volunteer.d.cts +1 -1
  98. package/dist/contact-volunteer.d.ts +1 -1
  99. package/dist/contact-warranty.d.cts +1 -1
  100. package/dist/contact-warranty.d.ts +1 -1
  101. package/dist/contact-wedding.d.cts +1 -1
  102. package/dist/contact-wedding.d.ts +1 -1
  103. package/dist/cta-app-download-newsletter.d.cts +1 -1
  104. package/dist/cta-app-download-newsletter.d.ts +1 -1
  105. package/dist/cta-newsletter-features.d.cts +1 -1
  106. package/dist/cta-newsletter-features.d.ts +1 -1
  107. package/dist/footer-accordion-social.d.cts +1 -1
  108. package/dist/footer-accordion-social.d.ts +1 -1
  109. package/dist/footer-newsletter-contact.d.cts +1 -1
  110. package/dist/footer-newsletter-contact.d.ts +1 -1
  111. package/dist/footer-newsletter-minimal.d.cts +1 -1
  112. package/dist/footer-newsletter-minimal.d.ts +1 -1
  113. package/dist/footer-split-image-accordion.d.cts +1 -1
  114. package/dist/footer-split-image-accordion.d.ts +1 -1
  115. package/dist/{forms-nGgHUTBw.d.cts → forms-CStlFhnh.d.cts} +41 -0
  116. package/dist/{forms-nGgHUTBw.d.ts → forms-CStlFhnh.d.ts} +41 -0
  117. package/dist/hero-conversation-intelligence.cjs +1 -2
  118. package/dist/hero-conversation-intelligence.d.cts +1 -5
  119. package/dist/hero-conversation-intelligence.d.ts +1 -5
  120. package/dist/hero-conversation-intelligence.js +1 -2
  121. package/dist/hero-conversion-video-play.cjs +2 -2
  122. package/dist/hero-conversion-video-play.js +2 -2
  123. package/dist/hero-design-system-3d.cjs +162 -82
  124. package/dist/hero-design-system-3d.js +162 -82
  125. package/dist/hero-ecommerce-product-showcase.cjs +103 -81
  126. package/dist/hero-ecommerce-product-showcase.d.cts +5 -1
  127. package/dist/hero-ecommerce-product-showcase.d.ts +5 -1
  128. package/dist/hero-ecommerce-product-showcase.js +103 -81
  129. package/dist/hero-floating-images.cjs +1 -1
  130. package/dist/hero-floating-images.js +1 -1
  131. package/dist/hero-hiring-animated-text.cjs +4 -4
  132. package/dist/hero-hiring-animated-text.js +4 -4
  133. package/dist/hero-minimal-centered-dark.cjs +111 -82
  134. package/dist/hero-minimal-centered-dark.d.cts +1 -1
  135. package/dist/hero-minimal-centered-dark.d.ts +1 -1
  136. package/dist/hero-minimal-centered-dark.js +111 -82
  137. package/dist/hero-mobile-app-download.cjs +1 -1
  138. package/dist/hero-mobile-app-download.js +1 -1
  139. package/dist/hero-overlay-cta-grid.cjs +1 -1
  140. package/dist/hero-overlay-cta-grid.js +1 -1
  141. package/dist/hero-spiral-pattern-cards.cjs +1 -1
  142. package/dist/hero-spiral-pattern-cards.js +1 -1
  143. package/dist/hero-startup-launch-cta.cjs +1 -1
  144. package/dist/hero-startup-launch-cta.js +1 -1
  145. package/dist/hero-stats-social-proof.cjs +106 -90
  146. package/dist/hero-stats-social-proof.js +106 -90
  147. package/dist/hero-testimonial-image-grid.cjs +1 -1
  148. package/dist/hero-testimonial-image-grid.js +1 -1
  149. package/dist/hero-therapy-testimonial-grid.cjs +1 -1
  150. package/dist/hero-therapy-testimonial-grid.js +1 -1
  151. package/dist/hero-ui-library-showcase.cjs +63 -15
  152. package/dist/hero-ui-library-showcase.d.cts +5 -1
  153. package/dist/hero-ui-library-showcase.d.ts +5 -1
  154. package/dist/hero-ui-library-showcase.js +63 -15
  155. package/dist/index.cjs +44 -6
  156. package/dist/index.d.cts +3 -2
  157. package/dist/index.d.ts +3 -2
  158. package/dist/index.js +44 -6
  159. package/dist/link-page-newsletter-social.d.cts +1 -1
  160. package/dist/link-page-newsletter-social.d.ts +1 -1
  161. package/dist/offer-modal-membership-image.d.cts +1 -1
  162. package/dist/offer-modal-membership-image.d.ts +1 -1
  163. package/dist/offer-modal-newsletter-discount.d.cts +1 -1
  164. package/dist/offer-modal-newsletter-discount.d.ts +1 -1
  165. package/dist/offer-modal-sheet-newsletter.d.cts +1 -1
  166. package/dist/offer-modal-sheet-newsletter.d.ts +1 -1
  167. package/dist/registry.cjs +14465 -14767
  168. package/dist/registry.js +12664 -12966
  169. package/dist/resource-list-hero-filter.d.cts +1 -1
  170. package/dist/resource-list-hero-filter.d.ts +1 -1
  171. package/package.json +3 -3
@@ -1,19 +1,15 @@
1
1
  "use client";
2
2
  import * as React from 'react';
3
- import React__default from 'react';
4
- import { useForm, Form, Field } from '@page-speed/forms';
5
- import { TextInput as TextInput$1, TextArea as TextArea$1 } from '@page-speed/forms/inputs';
3
+ import React__default, { useMemo, useState, useCallback } from 'react';
4
+ import { Form, useForm, Field } from '@page-speed/forms';
6
5
  import { clsx } from 'clsx';
7
6
  import { twMerge } from 'tailwind-merge';
8
7
  import { cva } from 'class-variance-authority';
9
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
- import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
11
- import * as LabelPrimitive from '@radix-ui/react-label';
9
+ import { TextInput, TextArea, Select, MultiSelect, Radio, Checkbox, CheckboxGroup, DatePicker, DateRangePicker, TimePicker, FileInput, RichTextEditor } from '@page-speed/forms/inputs';
12
10
  import { serializeForRails, deserializeErrors } from '@page-speed/forms/integration';
13
11
 
14
12
  // components/blocks/contact/contact-card.tsx
15
- var TextInput = TextInput$1;
16
- var TextArea = TextArea$1;
17
13
  function cn(...inputs) {
18
14
  return twMerge(clsx(inputs));
19
15
  }
@@ -360,6 +356,7 @@ var Pressable = React.forwardRef(
360
356
  rel,
361
357
  linkType,
362
358
  isInternal,
359
+ isExternal,
363
360
  handleClick
364
361
  } = navigation;
365
362
  const shouldRenderLink = normalizedHref && linkType !== "none";
@@ -553,46 +550,371 @@ function Card({ className, ...props }) {
553
550
  }
554
551
  );
555
552
  }
556
- function Checkbox({
553
+ function DynamicFormField({
554
+ field,
557
555
  className,
558
- ...props
556
+ uploadProgress = {},
557
+ onFileUpload,
558
+ onFileRemove,
559
+ isUploading = false
559
560
  }) {
561
+ const fieldId = field.name;
562
+ const usesGroupLegend = field.type === "radio" || field.type === "checkbox-group";
563
+ const usesInlineCheckboxLabel = field.type === "checkbox";
564
+ const shouldRenderFieldLabel = !usesGroupLegend && !usesInlineCheckboxLabel;
565
+ const checkboxLabel = /* @__PURE__ */ jsxs(Fragment, { children: [
566
+ field.label,
567
+ field.required ? /* @__PURE__ */ jsx("span", { className: "text-destructive ml-1", children: "*" }) : null
568
+ ] });
560
569
  return /* @__PURE__ */ jsx(
561
- CheckboxPrimitive.Root,
570
+ Field,
562
571
  {
563
- "data-slot": "checkbox",
564
- className: cn(
565
- "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
566
- className
567
- ),
568
- ...props,
569
- children: /* @__PURE__ */ jsx(
570
- CheckboxPrimitive.Indicator,
571
- {
572
- "data-slot": "checkbox-indicator",
573
- className: "grid place-content-center text-current transition-none",
574
- children: /* @__PURE__ */ jsx(DynamicIcon, { name: "lucide/check", size: 14 })
575
- }
576
- )
572
+ name: field.name,
573
+ label: shouldRenderFieldLabel ? field.label : void 0,
574
+ description: shouldRenderFieldLabel ? field.description : void 0,
575
+ required: field.required,
576
+ className: cn("space-y-2", className),
577
+ children: ({ field: formField, meta }) => /* @__PURE__ */ jsxs("div", { children: [
578
+ (field.type === "text" || field.type === "email" || field.type === "tel" || field.type === "search" || field.type === "password" || field.type === "url") && /* @__PURE__ */ jsx(
579
+ TextInput,
580
+ {
581
+ ...formField,
582
+ id: fieldId,
583
+ type: field.type,
584
+ placeholder: field.placeholder,
585
+ error: meta.touched && !!meta.error,
586
+ disabled: field.disabled,
587
+ "aria-label": field.label
588
+ }
589
+ ),
590
+ field.type === "number" && /* @__PURE__ */ jsx(
591
+ TextInput,
592
+ {
593
+ ...formField,
594
+ id: fieldId,
595
+ type: "text",
596
+ placeholder: field.placeholder,
597
+ error: meta.touched && !!meta.error,
598
+ disabled: field.disabled,
599
+ "aria-label": field.label
600
+ }
601
+ ),
602
+ field.type === "textarea" && /* @__PURE__ */ jsx(
603
+ TextArea,
604
+ {
605
+ ...formField,
606
+ id: fieldId,
607
+ placeholder: field.placeholder,
608
+ rows: field.rows || 4,
609
+ error: meta.touched && !!meta.error,
610
+ disabled: field.disabled,
611
+ "aria-label": field.label
612
+ }
613
+ ),
614
+ field.type === "select" && field.options && /* @__PURE__ */ jsx(
615
+ Select,
616
+ {
617
+ ...formField,
618
+ id: fieldId,
619
+ options: field.options,
620
+ placeholder: field.placeholder || `Select ${field.label.toLowerCase()}`,
621
+ error: meta.touched && !!meta.error,
622
+ disabled: field.disabled,
623
+ "aria-label": field.label
624
+ }
625
+ ),
626
+ field.type === "multi-select" && field.options && /* @__PURE__ */ jsx(
627
+ MultiSelect,
628
+ {
629
+ ...formField,
630
+ id: fieldId,
631
+ options: field.options,
632
+ placeholder: field.placeholder || `Select ${field.label.toLowerCase()}`,
633
+ error: meta.touched && !!meta.error,
634
+ disabled: field.disabled,
635
+ "aria-label": field.label
636
+ }
637
+ ),
638
+ field.type === "radio" && field.options && /* @__PURE__ */ jsx(
639
+ Radio,
640
+ {
641
+ ...formField,
642
+ id: fieldId,
643
+ options: field.options,
644
+ label: field.label,
645
+ description: field.description,
646
+ required: field.required,
647
+ disabled: field.disabled,
648
+ layout: field.layout || "stacked",
649
+ error: meta.touched && !!meta.error,
650
+ "aria-label": field.label
651
+ }
652
+ ),
653
+ field.type === "checkbox" && /* @__PURE__ */ jsx(
654
+ Checkbox,
655
+ {
656
+ ...formField,
657
+ id: fieldId,
658
+ value: formField.value === true || formField.value === "true",
659
+ onChange: (checked) => formField.onChange(checked),
660
+ label: checkboxLabel,
661
+ description: field.description,
662
+ disabled: field.disabled,
663
+ required: field.required,
664
+ error: meta.touched && !!meta.error,
665
+ "aria-label": field.label
666
+ }
667
+ ),
668
+ field.type === "checkbox-group" && field.options && /* @__PURE__ */ jsx(
669
+ CheckboxGroup,
670
+ {
671
+ ...formField,
672
+ id: fieldId,
673
+ options: field.options,
674
+ label: field.label,
675
+ description: field.description,
676
+ required: field.required,
677
+ disabled: field.disabled,
678
+ layout: field.layout || "stacked",
679
+ error: meta.touched && !!meta.error,
680
+ "aria-label": field.label
681
+ }
682
+ ),
683
+ (field.type === "date-picker" || field.type === "date") && /* @__PURE__ */ jsx(
684
+ DatePicker,
685
+ {
686
+ ...formField,
687
+ id: fieldId,
688
+ placeholder: field.placeholder,
689
+ error: meta.touched && !!meta.error,
690
+ disabled: field.disabled,
691
+ "aria-label": field.label
692
+ }
693
+ ),
694
+ field.type === "date-range" && /* @__PURE__ */ jsx(
695
+ DateRangePicker,
696
+ {
697
+ ...formField,
698
+ id: fieldId,
699
+ placeholder: field.placeholder,
700
+ error: meta.touched && !!meta.error,
701
+ disabled: field.disabled,
702
+ "aria-label": field.label
703
+ }
704
+ ),
705
+ field.type === "time" && /* @__PURE__ */ jsx(
706
+ TimePicker,
707
+ {
708
+ ...formField,
709
+ id: fieldId,
710
+ placeholder: field.placeholder,
711
+ error: meta.touched && !!meta.error,
712
+ disabled: field.disabled,
713
+ "aria-label": field.label
714
+ }
715
+ ),
716
+ field.type === "file" && /* @__PURE__ */ jsx(
717
+ FileInput,
718
+ {
719
+ ...formField,
720
+ id: fieldId,
721
+ accept: field.accept,
722
+ maxSize: field.maxSize || 5 * 1024 * 1024,
723
+ maxFiles: field.maxFiles || 1,
724
+ multiple: field.multiple || false,
725
+ placeholder: field.placeholder || "Choose file(s)...",
726
+ error: meta.touched && !!meta.error,
727
+ disabled: field.disabled || isUploading,
728
+ showProgress: true,
729
+ uploadProgress,
730
+ onChange: (files) => {
731
+ formField.onChange(files);
732
+ if (files.length > 0 && onFileUpload) {
733
+ onFileUpload(files);
734
+ }
735
+ },
736
+ onFileRemove,
737
+ "aria-label": field.label
738
+ }
739
+ ),
740
+ field.type === "rich-text" && /* @__PURE__ */ jsx(
741
+ RichTextEditor,
742
+ {
743
+ ...formField,
744
+ id: fieldId,
745
+ placeholder: field.placeholder,
746
+ error: meta.touched && !!meta.error,
747
+ disabled: field.disabled,
748
+ "aria-label": field.label
749
+ }
750
+ )
751
+ ] })
577
752
  }
578
753
  );
579
754
  }
580
- function Label({
581
- className,
582
- ...props
583
- }) {
584
- return /* @__PURE__ */ jsx(
585
- LabelPrimitive.Root,
586
- {
587
- "data-slot": "label",
588
- className: cn(
589
- "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
590
- className
591
- ),
592
- ...props
593
- }
755
+
756
+ // lib/form-field-types.ts
757
+ function generateInitialValues(fields) {
758
+ return fields.reduce(
759
+ (acc, field) => {
760
+ if (field.type === "checkbox") {
761
+ acc[field.name] = false;
762
+ } else if (field.type === "checkbox-group" || field.type === "multi-select") {
763
+ acc[field.name] = [];
764
+ } else if (field.type === "file") {
765
+ acc[field.name] = [];
766
+ } else if (field.type === "date-range") {
767
+ acc[field.name] = { start: null, end: null };
768
+ } else {
769
+ acc[field.name] = "";
770
+ }
771
+ return acc;
772
+ },
773
+ {}
774
+ );
775
+ }
776
+ function generateValidationSchema(fields) {
777
+ return fields.reduce(
778
+ (acc, field) => {
779
+ acc[field.name] = (value, allValues) => {
780
+ if (field.required) {
781
+ if (!value || typeof value === "string" && !value.trim()) {
782
+ return `${field.label} is required`;
783
+ }
784
+ }
785
+ if (field.type === "email" && value) {
786
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
787
+ return "Please enter a valid email address";
788
+ }
789
+ }
790
+ if (field.type === "url" && value) {
791
+ try {
792
+ new URL(value);
793
+ } catch {
794
+ return "Please enter a valid URL";
795
+ }
796
+ }
797
+ if (field.validator) {
798
+ return field.validator(value, allValues);
799
+ }
800
+ return void 0;
801
+ };
802
+ return acc;
803
+ },
804
+ {}
594
805
  );
595
806
  }
807
+ function getColumnSpanClass(span) {
808
+ if (!span || span === 12) return "col-span-12";
809
+ return `col-span-12 sm:col-span-${Math.min(span, 12)}`;
810
+ }
811
+ function useContactForm(options) {
812
+ const {
813
+ formFields,
814
+ formConfig,
815
+ onSubmit,
816
+ onSuccess,
817
+ onError,
818
+ resetOnSuccess = true,
819
+ uploadTokens = []
820
+ } = options;
821
+ const [submissionError, setSubmissionError] = useState(null);
822
+ const submissionConfig = formConfig?.submissionConfig;
823
+ const redirectUrl = submissionConfig?.redirectUrl;
824
+ const redirectNavigation = useNavigation({ href: redirectUrl });
825
+ const resetSubmissionState = useCallback(() => {
826
+ setSubmissionError(null);
827
+ }, []);
828
+ const performRedirect = useCallback(() => {
829
+ if (!redirectUrl || typeof window === "undefined") {
830
+ return;
831
+ }
832
+ const navigate = () => {
833
+ if (redirectNavigation.shouldUseRouter && redirectNavigation.normalizedHref) {
834
+ const handler = window.__opensiteNavigationHandler;
835
+ if (typeof handler === "function") {
836
+ try {
837
+ const handled = handler(redirectNavigation.normalizedHref, void 0);
838
+ if (handled !== false) {
839
+ return;
840
+ }
841
+ } catch (error) {
842
+ console.error("Internal redirect handler failed:", error);
843
+ }
844
+ }
845
+ }
846
+ const destination = redirectNavigation.normalizedHref || redirectUrl;
847
+ window.location.assign(destination);
848
+ };
849
+ window.setTimeout(navigate, 150);
850
+ }, [redirectNavigation, redirectUrl]);
851
+ const form = useForm({
852
+ initialValues: useMemo(
853
+ () => generateInitialValues(formFields),
854
+ [formFields]
855
+ ),
856
+ validationSchema: useMemo(
857
+ () => generateValidationSchema(formFields),
858
+ [formFields]
859
+ ),
860
+ onSubmit: async (values, helpers) => {
861
+ resetSubmissionState();
862
+ const shouldAutoSubmit = Boolean(formConfig?.endpoint);
863
+ if (!shouldAutoSubmit && !onSubmit) {
864
+ return;
865
+ }
866
+ try {
867
+ let result;
868
+ const submissionValues = {
869
+ ...values,
870
+ ...uploadTokens.length > 0 && {
871
+ contact_form_upload_tokens: uploadTokens
872
+ }
873
+ };
874
+ if (shouldAutoSubmit) {
875
+ result = await submitPageSpeedForm(submissionValues, formConfig);
876
+ }
877
+ if (onSubmit) {
878
+ await onSubmit(submissionValues);
879
+ }
880
+ if (shouldAutoSubmit || onSubmit) {
881
+ try {
882
+ await submissionConfig?.handleFormSubmission?.({
883
+ formData: submissionValues,
884
+ responseData: result
885
+ });
886
+ } catch (callbackError) {
887
+ console.error("handleFormSubmission callback failed:", callbackError);
888
+ }
889
+ if (resetOnSuccess) {
890
+ helpers.resetForm();
891
+ }
892
+ onSuccess?.(result);
893
+ if (submissionConfig?.behavior === "redirect" && submissionConfig.redirectUrl) {
894
+ performRedirect();
895
+ }
896
+ }
897
+ } catch (error) {
898
+ if (error instanceof PageSpeedFormSubmissionError && error.formErrors) {
899
+ helpers.setErrors(error.formErrors);
900
+ }
901
+ const errorMessage = error instanceof Error ? error.message : "Form submission failed";
902
+ setSubmissionError(errorMessage);
903
+ onError?.(error);
904
+ }
905
+ }
906
+ });
907
+ const formMethod = formConfig?.method?.toLowerCase() === "get" ? "get" : "post";
908
+ return {
909
+ form,
910
+ isSubmitted: form.status === "success",
911
+ submissionError,
912
+ formMethod,
913
+ resetSubmissionState
914
+ };
915
+ }
916
+
917
+ // lib/forms.ts
596
918
  var PageSpeedFormSubmissionError = class extends Error {
597
919
  constructor(message, options = {}) {
598
920
  super(message);
@@ -1083,84 +1405,92 @@ var Section = React__default.forwardRef(
1083
1405
  }
1084
1406
  );
1085
1407
  Section.displayName = "Section";
1408
+ var DEFAULT_FORM_FIELDS = [
1409
+ {
1410
+ name: "firstName",
1411
+ type: "text",
1412
+ label: "First Name",
1413
+ placeholder: "John",
1414
+ required: true,
1415
+ columnSpan: 6
1416
+ },
1417
+ {
1418
+ name: "lastName",
1419
+ type: "text",
1420
+ label: "Last Name",
1421
+ placeholder: "Doe",
1422
+ required: true,
1423
+ columnSpan: 6
1424
+ },
1425
+ {
1426
+ name: "email",
1427
+ type: "email",
1428
+ label: "Email Address",
1429
+ placeholder: "john@example.com",
1430
+ required: true,
1431
+ columnSpan: 12
1432
+ },
1433
+ {
1434
+ name: "message",
1435
+ type: "textarea",
1436
+ label: "Message",
1437
+ placeholder: "How can we help you?",
1438
+ required: true,
1439
+ rows: 4,
1440
+ columnSpan: 12
1441
+ },
1442
+ {
1443
+ name: "privacyPolicy",
1444
+ type: "checkbox",
1445
+ label: "I agree to the privacy policy",
1446
+ required: true,
1447
+ columnSpan: 12
1448
+ }
1449
+ ];
1086
1450
  function ContactCard({
1087
1451
  heading,
1088
1452
  description,
1089
1453
  formHeading,
1090
- buttonText,
1454
+ buttonText = "Send Message",
1091
1455
  buttonIcon,
1092
1456
  actions,
1093
1457
  actionsSlot,
1094
1458
  contactOptions,
1095
1459
  contactOptionsSlot,
1460
+ formFields,
1461
+ successMessage = "Thank you! We'll be in touch soon.",
1096
1462
  className,
1097
- containerClassName,
1463
+ containerClassName = "px-6 sm:px-6 md:px-8 lg:px-8",
1098
1464
  cardClassName,
1099
1465
  formHeadingClassName,
1100
1466
  formClassName,
1101
1467
  submitClassName,
1468
+ successMessageClassName,
1469
+ errorMessageClassName,
1102
1470
  infoPanelClassName,
1103
1471
  headingClassName,
1104
1472
  descriptionClassName,
1105
1473
  contactOptionsClassName,
1106
- background = "white",
1107
- spacing = "xl",
1474
+ background,
1475
+ spacing = "py-8 md:py-32",
1108
1476
  pattern,
1109
- patternOpacity = 0.1,
1477
+ patternOpacity,
1110
1478
  formConfig,
1111
1479
  onSubmit,
1112
1480
  onSuccess,
1113
1481
  onError
1114
1482
  }) {
1115
- const form = useForm({
1116
- initialValues: {
1117
- firstName: "",
1118
- lastName: "",
1119
- email: "",
1120
- message: "",
1121
- privacyPolicy: false
1122
- },
1123
- validationSchema: {
1124
- firstName: (value) => !value ? "First name is required" : void 0,
1125
- lastName: (value) => !value ? "Last name is required" : void 0,
1126
- email: (value) => {
1127
- if (!value) return "Email is required";
1128
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
1129
- return "Please enter a valid email address";
1130
- return void 0;
1131
- },
1132
- message: (value) => !value ? "Message is required" : void 0,
1133
- privacyPolicy: (value) => !value ? "You must agree to the privacy policy" : void 0
1134
- },
1135
- onSubmit: async (values, helpers) => {
1136
- const shouldAutoSubmit = Boolean(formConfig?.endpoint);
1137
- if (!shouldAutoSubmit && !onSubmit) {
1138
- return;
1139
- }
1140
- try {
1141
- let result;
1142
- if (shouldAutoSubmit) {
1143
- result = await submitPageSpeedForm(values, formConfig);
1144
- }
1145
- if (onSubmit) {
1146
- await onSubmit(values);
1147
- }
1148
- if (shouldAutoSubmit || onSubmit) {
1149
- if (formConfig?.resetOnSuccess !== false) {
1150
- helpers.resetForm();
1151
- }
1152
- onSuccess?.(result);
1153
- }
1154
- } catch (error) {
1155
- if (error instanceof PageSpeedFormSubmissionError && error.formErrors) {
1156
- helpers.setErrors(error.formErrors);
1157
- }
1158
- onError?.(error);
1159
- throw error;
1160
- }
1161
- }
1483
+ const fields = useMemo(
1484
+ () => formFields || DEFAULT_FORM_FIELDS,
1485
+ [formFields]
1486
+ );
1487
+ const { form, submissionError, formMethod, resetSubmissionState } = useContactForm({
1488
+ formFields: fields,
1489
+ formConfig,
1490
+ onSubmit,
1491
+ onSuccess,
1492
+ onError
1162
1493
  });
1163
- const formMethod = formConfig?.method?.toLowerCase() === "get" ? "get" : "post";
1164
1494
  const actionsContent = React.useMemo(() => {
1165
1495
  if (actionsSlot) return actionsSlot;
1166
1496
  if (actions && actions.length > 0) {
@@ -1184,18 +1514,11 @@ function ContactCard({
1184
1514
  }
1185
1515
  return null;
1186
1516
  }, [actionsSlot, actions]);
1187
- const contactOptionsContent = React.useMemo(() => {
1517
+ const contactOptionsContent = useMemo(() => {
1188
1518
  if (contactOptionsSlot) return contactOptionsSlot;
1189
1519
  if (contactOptions && contactOptions.length > 0) {
1190
1520
  return contactOptions.map((option, key) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
1191
- /* @__PURE__ */ jsx(
1192
- DynamicIcon,
1193
- {
1194
- name: option.icon,
1195
- size: 20,
1196
- className: "text-muted-foreground"
1197
- }
1198
- ),
1521
+ /* @__PURE__ */ jsx(DynamicIcon, { name: option.icon, size: 20 }),
1199
1522
  option.href ? /* @__PURE__ */ jsx(Pressable, { href: option.href, children: option.info }) : /* @__PURE__ */ jsx("span", { children: option.info })
1200
1523
  ] }, key));
1201
1524
  }
@@ -1208,96 +1531,42 @@ function ContactCard({
1208
1531
  spacing,
1209
1532
  pattern,
1210
1533
  patternOpacity,
1211
- className: cn("py-12", className),
1212
- children: /* @__PURE__ */ jsx("div", { className: cn("mx-auto w-full max-w-4xl px-4", containerClassName), children: /* @__PURE__ */ jsxs("div", { className: "grid items-start gap-10 lg:grid-cols-2", children: [
1534
+ className,
1535
+ containerClassName,
1536
+ children: /* @__PURE__ */ jsx("div", { className: "relative", children: /* @__PURE__ */ jsxs("div", { className: "grid items-start gap-10 lg:grid-cols-2", children: [
1213
1537
  /* @__PURE__ */ jsxs(Card, { className: cn("p-6 lg:p-8", cardClassName), children: [
1214
- formHeading && (typeof formHeading === "string" ? /* @__PURE__ */ jsx("h3", { className: cn("mb-6 text-2xl font-semibold tracking-tight", formHeadingClassName), children: formHeading }) : /* @__PURE__ */ jsx("div", { className: formHeadingClassName, children: formHeading })),
1538
+ formHeading && (typeof formHeading === "string" ? /* @__PURE__ */ jsx(
1539
+ "h3",
1540
+ {
1541
+ className: cn(
1542
+ "mb-6 text-2xl font-semibold tracking-tight",
1543
+ formHeadingClassName
1544
+ ),
1545
+ children: formHeading
1546
+ }
1547
+ ) : /* @__PURE__ */ jsx("div", { className: formHeadingClassName, children: formHeading })),
1215
1548
  /* @__PURE__ */ jsxs(
1216
1549
  Form,
1217
1550
  {
1218
1551
  form,
1219
1552
  action: formConfig?.endpoint,
1220
1553
  method: formMethod,
1554
+ submissionError,
1555
+ successMessage,
1556
+ successMessageClassName,
1557
+ errorMessageClassName,
1558
+ submissionConfig: formConfig?.submissionConfig,
1559
+ onNewSubmission: resetSubmissionState,
1221
1560
  className: cn("space-y-6", formClassName),
1222
1561
  children: [
1223
- /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
1224
- /* @__PURE__ */ jsx(Field, { name: "firstName", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1225
- /* @__PURE__ */ jsx(Label, { htmlFor: "first-name", children: "First Name" }),
1226
- /* @__PURE__ */ jsx(
1227
- TextInput,
1228
- {
1229
- ...field,
1230
- id: "first-name",
1231
- placeholder: "John",
1232
- error: meta.touched && !!meta.error,
1233
- "aria-label": "First Name"
1234
- }
1235
- )
1236
- ] }) }),
1237
- /* @__PURE__ */ jsx(Field, { name: "lastName", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1238
- /* @__PURE__ */ jsx(Label, { htmlFor: "last-name", children: "Last Name" }),
1239
- /* @__PURE__ */ jsx(
1240
- TextInput,
1241
- {
1242
- ...field,
1243
- id: "last-name",
1244
- placeholder: "Doe",
1245
- error: meta.touched && !!meta.error,
1246
- "aria-label": "Last Name"
1247
- }
1248
- )
1249
- ] }) })
1250
- ] }),
1251
- /* @__PURE__ */ jsx(Field, { name: "email", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1252
- /* @__PURE__ */ jsx(Label, { htmlFor: "email", children: "Email Address" }),
1253
- /* @__PURE__ */ jsx(
1254
- TextInput,
1255
- {
1256
- ...field,
1257
- id: "email",
1258
- type: "email",
1259
- placeholder: "john@example.com",
1260
- error: meta.touched && !!meta.error,
1261
- "aria-label": "Email Address"
1262
- }
1263
- )
1264
- ] }) }),
1265
- /* @__PURE__ */ jsx(Field, { name: "message", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1266
- /* @__PURE__ */ jsx(Label, { htmlFor: "message", children: "Message" }),
1267
- /* @__PURE__ */ jsx(
1268
- TextArea,
1269
- {
1270
- ...field,
1271
- id: "message",
1272
- placeholder: "Tell us how we can help...",
1273
- rows: 4,
1274
- error: meta.touched && !!meta.error,
1275
- "aria-label": "Message"
1276
- }
1277
- )
1278
- ] }) }),
1279
- /* @__PURE__ */ jsx(Field, { name: "privacyPolicy", children: ({ field }) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1280
- /* @__PURE__ */ jsx(
1281
- Checkbox,
1282
- {
1283
- id: "privacy-policy",
1284
- checked: field.value,
1285
- onCheckedChange: (checked) => field.onChange(checked === true)
1286
- }
1287
- ),
1288
- /* @__PURE__ */ jsxs(
1289
- Label,
1290
- {
1291
- htmlFor: "privacy-policy",
1292
- className: "cursor-pointer text-sm font-normal",
1293
- children: [
1294
- "I agree to the",
1295
- " ",
1296
- /* @__PURE__ */ jsx(Pressable, { href: "#", className: "text-primary hover:underline", children: "Privacy Policy" })
1297
- ]
1298
- }
1299
- )
1300
- ] }) }),
1562
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-12 gap-6", children: fields.map((field) => /* @__PURE__ */ jsx(
1563
+ "div",
1564
+ {
1565
+ className: getColumnSpanClass(field.columnSpan),
1566
+ children: /* @__PURE__ */ jsx(DynamicFormField, { field })
1567
+ },
1568
+ field.name
1569
+ )) }),
1301
1570
  actionsSlot || actions && actions.length > 0 ? actionsContent : /* @__PURE__ */ jsxs(
1302
1571
  Pressable,
1303
1572
  {
@@ -1317,8 +1586,17 @@ function ContactCard({
1317
1586
  )
1318
1587
  ] }),
1319
1588
  /* @__PURE__ */ jsxs("div", { className: cn("lg:pt-8", infoPanelClassName), children: [
1320
- heading && (typeof heading === "string" ? /* @__PURE__ */ jsx("h2", { className: cn("mb-3 text-3xl font-bold tracking-tight", headingClassName), children: heading }) : /* @__PURE__ */ jsx("div", { className: headingClassName, children: heading })),
1321
- description && (typeof description === "string" ? /* @__PURE__ */ jsx("p", { className: cn("leading-relaxed text-muted-foreground", descriptionClassName), children: description }) : /* @__PURE__ */ jsx("div", { className: descriptionClassName, children: description })),
1589
+ heading && (typeof heading === "string" ? /* @__PURE__ */ jsx(
1590
+ "h2",
1591
+ {
1592
+ className: cn(
1593
+ "mb-3 text-3xl font-bold tracking-tight",
1594
+ headingClassName
1595
+ ),
1596
+ children: heading
1597
+ }
1598
+ ) : /* @__PURE__ */ jsx("div", { className: headingClassName, children: heading })),
1599
+ description && (typeof description === "string" ? /* @__PURE__ */ jsx("p", { className: cn("leading-relaxed", descriptionClassName), children: description }) : /* @__PURE__ */ jsx("div", { className: descriptionClassName, children: description })),
1322
1600
  /* @__PURE__ */ jsx("div", { className: cn("mt-10 space-y-4", contactOptionsClassName), children: contactOptionsContent })
1323
1601
  ] })
1324
1602
  ] }) })