@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
@@ -3,13 +3,11 @@
3
3
 
4
4
  var React = require('react');
5
5
  var forms = require('@page-speed/forms');
6
- var inputs = require('@page-speed/forms/inputs');
7
6
  var clsx = require('clsx');
8
7
  var tailwindMerge = require('tailwind-merge');
9
8
  var classVarianceAuthority = require('class-variance-authority');
10
9
  var jsxRuntime = require('react/jsx-runtime');
11
- var CheckboxPrimitive = require('@radix-ui/react-checkbox');
12
- var LabelPrimitive = require('@radix-ui/react-label');
10
+ var inputs = require('@page-speed/forms/inputs');
13
11
  var integration = require('@page-speed/forms/integration');
14
12
 
15
13
  function _interopNamespace(e) {
@@ -31,12 +29,8 @@ function _interopNamespace(e) {
31
29
  }
32
30
 
33
31
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
34
- var CheckboxPrimitive__namespace = /*#__PURE__*/_interopNamespace(CheckboxPrimitive);
35
- var LabelPrimitive__namespace = /*#__PURE__*/_interopNamespace(LabelPrimitive);
36
32
 
37
33
  // components/blocks/contact/contact-card.tsx
38
- var TextInput = inputs.TextInput;
39
- var TextArea = inputs.TextArea;
40
34
  function cn(...inputs) {
41
35
  return tailwindMerge.twMerge(clsx.clsx(inputs));
42
36
  }
@@ -383,6 +377,7 @@ var Pressable = React__namespace.forwardRef(
383
377
  rel,
384
378
  linkType,
385
379
  isInternal,
380
+ isExternal,
386
381
  handleClick
387
382
  } = navigation;
388
383
  const shouldRenderLink = normalizedHref && linkType !== "none";
@@ -576,46 +571,371 @@ function Card({ className, ...props }) {
576
571
  }
577
572
  );
578
573
  }
579
- function Checkbox({
574
+ function DynamicFormField({
575
+ field,
580
576
  className,
581
- ...props
577
+ uploadProgress = {},
578
+ onFileUpload,
579
+ onFileRemove,
580
+ isUploading = false
582
581
  }) {
582
+ const fieldId = field.name;
583
+ const usesGroupLegend = field.type === "radio" || field.type === "checkbox-group";
584
+ const usesInlineCheckboxLabel = field.type === "checkbox";
585
+ const shouldRenderFieldLabel = !usesGroupLegend && !usesInlineCheckboxLabel;
586
+ const checkboxLabel = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
587
+ field.label,
588
+ field.required ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-destructive ml-1", children: "*" }) : null
589
+ ] });
583
590
  return /* @__PURE__ */ jsxRuntime.jsx(
584
- CheckboxPrimitive__namespace.Root,
591
+ forms.Field,
585
592
  {
586
- "data-slot": "checkbox",
587
- className: cn(
588
- "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",
589
- className
590
- ),
591
- ...props,
592
- children: /* @__PURE__ */ jsxRuntime.jsx(
593
- CheckboxPrimitive__namespace.Indicator,
594
- {
595
- "data-slot": "checkbox-indicator",
596
- className: "grid place-content-center text-current transition-none",
597
- children: /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: "lucide/check", size: 14 })
598
- }
599
- )
593
+ name: field.name,
594
+ label: shouldRenderFieldLabel ? field.label : void 0,
595
+ description: shouldRenderFieldLabel ? field.description : void 0,
596
+ required: field.required,
597
+ className: cn("space-y-2", className),
598
+ children: ({ field: formField, meta }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
599
+ (field.type === "text" || field.type === "email" || field.type === "tel" || field.type === "search" || field.type === "password" || field.type === "url") && /* @__PURE__ */ jsxRuntime.jsx(
600
+ inputs.TextInput,
601
+ {
602
+ ...formField,
603
+ id: fieldId,
604
+ type: field.type,
605
+ placeholder: field.placeholder,
606
+ error: meta.touched && !!meta.error,
607
+ disabled: field.disabled,
608
+ "aria-label": field.label
609
+ }
610
+ ),
611
+ field.type === "number" && /* @__PURE__ */ jsxRuntime.jsx(
612
+ inputs.TextInput,
613
+ {
614
+ ...formField,
615
+ id: fieldId,
616
+ type: "text",
617
+ placeholder: field.placeholder,
618
+ error: meta.touched && !!meta.error,
619
+ disabled: field.disabled,
620
+ "aria-label": field.label
621
+ }
622
+ ),
623
+ field.type === "textarea" && /* @__PURE__ */ jsxRuntime.jsx(
624
+ inputs.TextArea,
625
+ {
626
+ ...formField,
627
+ id: fieldId,
628
+ placeholder: field.placeholder,
629
+ rows: field.rows || 4,
630
+ error: meta.touched && !!meta.error,
631
+ disabled: field.disabled,
632
+ "aria-label": field.label
633
+ }
634
+ ),
635
+ field.type === "select" && field.options && /* @__PURE__ */ jsxRuntime.jsx(
636
+ inputs.Select,
637
+ {
638
+ ...formField,
639
+ id: fieldId,
640
+ options: field.options,
641
+ placeholder: field.placeholder || `Select ${field.label.toLowerCase()}`,
642
+ error: meta.touched && !!meta.error,
643
+ disabled: field.disabled,
644
+ "aria-label": field.label
645
+ }
646
+ ),
647
+ field.type === "multi-select" && field.options && /* @__PURE__ */ jsxRuntime.jsx(
648
+ inputs.MultiSelect,
649
+ {
650
+ ...formField,
651
+ id: fieldId,
652
+ options: field.options,
653
+ placeholder: field.placeholder || `Select ${field.label.toLowerCase()}`,
654
+ error: meta.touched && !!meta.error,
655
+ disabled: field.disabled,
656
+ "aria-label": field.label
657
+ }
658
+ ),
659
+ field.type === "radio" && field.options && /* @__PURE__ */ jsxRuntime.jsx(
660
+ inputs.Radio,
661
+ {
662
+ ...formField,
663
+ id: fieldId,
664
+ options: field.options,
665
+ label: field.label,
666
+ description: field.description,
667
+ required: field.required,
668
+ disabled: field.disabled,
669
+ layout: field.layout || "stacked",
670
+ error: meta.touched && !!meta.error,
671
+ "aria-label": field.label
672
+ }
673
+ ),
674
+ field.type === "checkbox" && /* @__PURE__ */ jsxRuntime.jsx(
675
+ inputs.Checkbox,
676
+ {
677
+ ...formField,
678
+ id: fieldId,
679
+ value: formField.value === true || formField.value === "true",
680
+ onChange: (checked) => formField.onChange(checked),
681
+ label: checkboxLabel,
682
+ description: field.description,
683
+ disabled: field.disabled,
684
+ required: field.required,
685
+ error: meta.touched && !!meta.error,
686
+ "aria-label": field.label
687
+ }
688
+ ),
689
+ field.type === "checkbox-group" && field.options && /* @__PURE__ */ jsxRuntime.jsx(
690
+ inputs.CheckboxGroup,
691
+ {
692
+ ...formField,
693
+ id: fieldId,
694
+ options: field.options,
695
+ label: field.label,
696
+ description: field.description,
697
+ required: field.required,
698
+ disabled: field.disabled,
699
+ layout: field.layout || "stacked",
700
+ error: meta.touched && !!meta.error,
701
+ "aria-label": field.label
702
+ }
703
+ ),
704
+ (field.type === "date-picker" || field.type === "date") && /* @__PURE__ */ jsxRuntime.jsx(
705
+ inputs.DatePicker,
706
+ {
707
+ ...formField,
708
+ id: fieldId,
709
+ placeholder: field.placeholder,
710
+ error: meta.touched && !!meta.error,
711
+ disabled: field.disabled,
712
+ "aria-label": field.label
713
+ }
714
+ ),
715
+ field.type === "date-range" && /* @__PURE__ */ jsxRuntime.jsx(
716
+ inputs.DateRangePicker,
717
+ {
718
+ ...formField,
719
+ id: fieldId,
720
+ placeholder: field.placeholder,
721
+ error: meta.touched && !!meta.error,
722
+ disabled: field.disabled,
723
+ "aria-label": field.label
724
+ }
725
+ ),
726
+ field.type === "time" && /* @__PURE__ */ jsxRuntime.jsx(
727
+ inputs.TimePicker,
728
+ {
729
+ ...formField,
730
+ id: fieldId,
731
+ placeholder: field.placeholder,
732
+ error: meta.touched && !!meta.error,
733
+ disabled: field.disabled,
734
+ "aria-label": field.label
735
+ }
736
+ ),
737
+ field.type === "file" && /* @__PURE__ */ jsxRuntime.jsx(
738
+ inputs.FileInput,
739
+ {
740
+ ...formField,
741
+ id: fieldId,
742
+ accept: field.accept,
743
+ maxSize: field.maxSize || 5 * 1024 * 1024,
744
+ maxFiles: field.maxFiles || 1,
745
+ multiple: field.multiple || false,
746
+ placeholder: field.placeholder || "Choose file(s)...",
747
+ error: meta.touched && !!meta.error,
748
+ disabled: field.disabled || isUploading,
749
+ showProgress: true,
750
+ uploadProgress,
751
+ onChange: (files) => {
752
+ formField.onChange(files);
753
+ if (files.length > 0 && onFileUpload) {
754
+ onFileUpload(files);
755
+ }
756
+ },
757
+ onFileRemove,
758
+ "aria-label": field.label
759
+ }
760
+ ),
761
+ field.type === "rich-text" && /* @__PURE__ */ jsxRuntime.jsx(
762
+ inputs.RichTextEditor,
763
+ {
764
+ ...formField,
765
+ id: fieldId,
766
+ placeholder: field.placeholder,
767
+ error: meta.touched && !!meta.error,
768
+ disabled: field.disabled,
769
+ "aria-label": field.label
770
+ }
771
+ )
772
+ ] })
600
773
  }
601
774
  );
602
775
  }
603
- function Label({
604
- className,
605
- ...props
606
- }) {
607
- return /* @__PURE__ */ jsxRuntime.jsx(
608
- LabelPrimitive__namespace.Root,
609
- {
610
- "data-slot": "label",
611
- className: cn(
612
- "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",
613
- className
614
- ),
615
- ...props
616
- }
776
+
777
+ // lib/form-field-types.ts
778
+ function generateInitialValues(fields) {
779
+ return fields.reduce(
780
+ (acc, field) => {
781
+ if (field.type === "checkbox") {
782
+ acc[field.name] = false;
783
+ } else if (field.type === "checkbox-group" || field.type === "multi-select") {
784
+ acc[field.name] = [];
785
+ } else if (field.type === "file") {
786
+ acc[field.name] = [];
787
+ } else if (field.type === "date-range") {
788
+ acc[field.name] = { start: null, end: null };
789
+ } else {
790
+ acc[field.name] = "";
791
+ }
792
+ return acc;
793
+ },
794
+ {}
795
+ );
796
+ }
797
+ function generateValidationSchema(fields) {
798
+ return fields.reduce(
799
+ (acc, field) => {
800
+ acc[field.name] = (value, allValues) => {
801
+ if (field.required) {
802
+ if (!value || typeof value === "string" && !value.trim()) {
803
+ return `${field.label} is required`;
804
+ }
805
+ }
806
+ if (field.type === "email" && value) {
807
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
808
+ return "Please enter a valid email address";
809
+ }
810
+ }
811
+ if (field.type === "url" && value) {
812
+ try {
813
+ new URL(value);
814
+ } catch {
815
+ return "Please enter a valid URL";
816
+ }
817
+ }
818
+ if (field.validator) {
819
+ return field.validator(value, allValues);
820
+ }
821
+ return void 0;
822
+ };
823
+ return acc;
824
+ },
825
+ {}
617
826
  );
618
827
  }
828
+ function getColumnSpanClass(span) {
829
+ if (!span || span === 12) return "col-span-12";
830
+ return `col-span-12 sm:col-span-${Math.min(span, 12)}`;
831
+ }
832
+ function useContactForm(options) {
833
+ const {
834
+ formFields,
835
+ formConfig,
836
+ onSubmit,
837
+ onSuccess,
838
+ onError,
839
+ resetOnSuccess = true,
840
+ uploadTokens = []
841
+ } = options;
842
+ const [submissionError, setSubmissionError] = React.useState(null);
843
+ const submissionConfig = formConfig?.submissionConfig;
844
+ const redirectUrl = submissionConfig?.redirectUrl;
845
+ const redirectNavigation = useNavigation({ href: redirectUrl });
846
+ const resetSubmissionState = React.useCallback(() => {
847
+ setSubmissionError(null);
848
+ }, []);
849
+ const performRedirect = React.useCallback(() => {
850
+ if (!redirectUrl || typeof window === "undefined") {
851
+ return;
852
+ }
853
+ const navigate = () => {
854
+ if (redirectNavigation.shouldUseRouter && redirectNavigation.normalizedHref) {
855
+ const handler = window.__opensiteNavigationHandler;
856
+ if (typeof handler === "function") {
857
+ try {
858
+ const handled = handler(redirectNavigation.normalizedHref, void 0);
859
+ if (handled !== false) {
860
+ return;
861
+ }
862
+ } catch (error) {
863
+ console.error("Internal redirect handler failed:", error);
864
+ }
865
+ }
866
+ }
867
+ const destination = redirectNavigation.normalizedHref || redirectUrl;
868
+ window.location.assign(destination);
869
+ };
870
+ window.setTimeout(navigate, 150);
871
+ }, [redirectNavigation, redirectUrl]);
872
+ const form = forms.useForm({
873
+ initialValues: React.useMemo(
874
+ () => generateInitialValues(formFields),
875
+ [formFields]
876
+ ),
877
+ validationSchema: React.useMemo(
878
+ () => generateValidationSchema(formFields),
879
+ [formFields]
880
+ ),
881
+ onSubmit: async (values, helpers) => {
882
+ resetSubmissionState();
883
+ const shouldAutoSubmit = Boolean(formConfig?.endpoint);
884
+ if (!shouldAutoSubmit && !onSubmit) {
885
+ return;
886
+ }
887
+ try {
888
+ let result;
889
+ const submissionValues = {
890
+ ...values,
891
+ ...uploadTokens.length > 0 && {
892
+ contact_form_upload_tokens: uploadTokens
893
+ }
894
+ };
895
+ if (shouldAutoSubmit) {
896
+ result = await submitPageSpeedForm(submissionValues, formConfig);
897
+ }
898
+ if (onSubmit) {
899
+ await onSubmit(submissionValues);
900
+ }
901
+ if (shouldAutoSubmit || onSubmit) {
902
+ try {
903
+ await submissionConfig?.handleFormSubmission?.({
904
+ formData: submissionValues,
905
+ responseData: result
906
+ });
907
+ } catch (callbackError) {
908
+ console.error("handleFormSubmission callback failed:", callbackError);
909
+ }
910
+ if (resetOnSuccess) {
911
+ helpers.resetForm();
912
+ }
913
+ onSuccess?.(result);
914
+ if (submissionConfig?.behavior === "redirect" && submissionConfig.redirectUrl) {
915
+ performRedirect();
916
+ }
917
+ }
918
+ } catch (error) {
919
+ if (error instanceof PageSpeedFormSubmissionError && error.formErrors) {
920
+ helpers.setErrors(error.formErrors);
921
+ }
922
+ const errorMessage = error instanceof Error ? error.message : "Form submission failed";
923
+ setSubmissionError(errorMessage);
924
+ onError?.(error);
925
+ }
926
+ }
927
+ });
928
+ const formMethod = formConfig?.method?.toLowerCase() === "get" ? "get" : "post";
929
+ return {
930
+ form,
931
+ isSubmitted: form.status === "success",
932
+ submissionError,
933
+ formMethod,
934
+ resetSubmissionState
935
+ };
936
+ }
937
+
938
+ // lib/forms.ts
619
939
  var PageSpeedFormSubmissionError = class extends Error {
620
940
  constructor(message, options = {}) {
621
941
  super(message);
@@ -1106,84 +1426,92 @@ var Section = React__namespace.default.forwardRef(
1106
1426
  }
1107
1427
  );
1108
1428
  Section.displayName = "Section";
1429
+ var DEFAULT_FORM_FIELDS = [
1430
+ {
1431
+ name: "firstName",
1432
+ type: "text",
1433
+ label: "First Name",
1434
+ placeholder: "John",
1435
+ required: true,
1436
+ columnSpan: 6
1437
+ },
1438
+ {
1439
+ name: "lastName",
1440
+ type: "text",
1441
+ label: "Last Name",
1442
+ placeholder: "Doe",
1443
+ required: true,
1444
+ columnSpan: 6
1445
+ },
1446
+ {
1447
+ name: "email",
1448
+ type: "email",
1449
+ label: "Email Address",
1450
+ placeholder: "john@example.com",
1451
+ required: true,
1452
+ columnSpan: 12
1453
+ },
1454
+ {
1455
+ name: "message",
1456
+ type: "textarea",
1457
+ label: "Message",
1458
+ placeholder: "How can we help you?",
1459
+ required: true,
1460
+ rows: 4,
1461
+ columnSpan: 12
1462
+ },
1463
+ {
1464
+ name: "privacyPolicy",
1465
+ type: "checkbox",
1466
+ label: "I agree to the privacy policy",
1467
+ required: true,
1468
+ columnSpan: 12
1469
+ }
1470
+ ];
1109
1471
  function ContactCard({
1110
1472
  heading,
1111
1473
  description,
1112
1474
  formHeading,
1113
- buttonText,
1475
+ buttonText = "Send Message",
1114
1476
  buttonIcon,
1115
1477
  actions,
1116
1478
  actionsSlot,
1117
1479
  contactOptions,
1118
1480
  contactOptionsSlot,
1481
+ formFields,
1482
+ successMessage = "Thank you! We'll be in touch soon.",
1119
1483
  className,
1120
- containerClassName,
1484
+ containerClassName = "px-6 sm:px-6 md:px-8 lg:px-8",
1121
1485
  cardClassName,
1122
1486
  formHeadingClassName,
1123
1487
  formClassName,
1124
1488
  submitClassName,
1489
+ successMessageClassName,
1490
+ errorMessageClassName,
1125
1491
  infoPanelClassName,
1126
1492
  headingClassName,
1127
1493
  descriptionClassName,
1128
1494
  contactOptionsClassName,
1129
- background = "white",
1130
- spacing = "xl",
1495
+ background,
1496
+ spacing = "py-8 md:py-32",
1131
1497
  pattern,
1132
- patternOpacity = 0.1,
1498
+ patternOpacity,
1133
1499
  formConfig,
1134
1500
  onSubmit,
1135
1501
  onSuccess,
1136
1502
  onError
1137
1503
  }) {
1138
- const form = forms.useForm({
1139
- initialValues: {
1140
- firstName: "",
1141
- lastName: "",
1142
- email: "",
1143
- message: "",
1144
- privacyPolicy: false
1145
- },
1146
- validationSchema: {
1147
- firstName: (value) => !value ? "First name is required" : void 0,
1148
- lastName: (value) => !value ? "Last name is required" : void 0,
1149
- email: (value) => {
1150
- if (!value) return "Email is required";
1151
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
1152
- return "Please enter a valid email address";
1153
- return void 0;
1154
- },
1155
- message: (value) => !value ? "Message is required" : void 0,
1156
- privacyPolicy: (value) => !value ? "You must agree to the privacy policy" : void 0
1157
- },
1158
- onSubmit: async (values, helpers) => {
1159
- const shouldAutoSubmit = Boolean(formConfig?.endpoint);
1160
- if (!shouldAutoSubmit && !onSubmit) {
1161
- return;
1162
- }
1163
- try {
1164
- let result;
1165
- if (shouldAutoSubmit) {
1166
- result = await submitPageSpeedForm(values, formConfig);
1167
- }
1168
- if (onSubmit) {
1169
- await onSubmit(values);
1170
- }
1171
- if (shouldAutoSubmit || onSubmit) {
1172
- if (formConfig?.resetOnSuccess !== false) {
1173
- helpers.resetForm();
1174
- }
1175
- onSuccess?.(result);
1176
- }
1177
- } catch (error) {
1178
- if (error instanceof PageSpeedFormSubmissionError && error.formErrors) {
1179
- helpers.setErrors(error.formErrors);
1180
- }
1181
- onError?.(error);
1182
- throw error;
1183
- }
1184
- }
1504
+ const fields = React.useMemo(
1505
+ () => formFields || DEFAULT_FORM_FIELDS,
1506
+ [formFields]
1507
+ );
1508
+ const { form, submissionError, formMethod, resetSubmissionState } = useContactForm({
1509
+ formFields: fields,
1510
+ formConfig,
1511
+ onSubmit,
1512
+ onSuccess,
1513
+ onError
1185
1514
  });
1186
- const formMethod = formConfig?.method?.toLowerCase() === "get" ? "get" : "post";
1187
1515
  const actionsContent = React__namespace.useMemo(() => {
1188
1516
  if (actionsSlot) return actionsSlot;
1189
1517
  if (actions && actions.length > 0) {
@@ -1207,18 +1535,11 @@ function ContactCard({
1207
1535
  }
1208
1536
  return null;
1209
1537
  }, [actionsSlot, actions]);
1210
- const contactOptionsContent = React__namespace.useMemo(() => {
1538
+ const contactOptionsContent = React.useMemo(() => {
1211
1539
  if (contactOptionsSlot) return contactOptionsSlot;
1212
1540
  if (contactOptions && contactOptions.length > 0) {
1213
1541
  return contactOptions.map((option, key) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
1214
- /* @__PURE__ */ jsxRuntime.jsx(
1215
- DynamicIcon,
1216
- {
1217
- name: option.icon,
1218
- size: 20,
1219
- className: "text-muted-foreground"
1220
- }
1221
- ),
1542
+ /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: option.icon, size: 20 }),
1222
1543
  option.href ? /* @__PURE__ */ jsxRuntime.jsx(Pressable, { href: option.href, children: option.info }) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: option.info })
1223
1544
  ] }, key));
1224
1545
  }
@@ -1231,96 +1552,42 @@ function ContactCard({
1231
1552
  spacing,
1232
1553
  pattern,
1233
1554
  patternOpacity,
1234
- className: cn("py-12", className),
1235
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("mx-auto w-full max-w-4xl px-4", containerClassName), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid items-start gap-10 lg:grid-cols-2", children: [
1555
+ className,
1556
+ containerClassName,
1557
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid items-start gap-10 lg:grid-cols-2", children: [
1236
1558
  /* @__PURE__ */ jsxRuntime.jsxs(Card, { className: cn("p-6 lg:p-8", cardClassName), children: [
1237
- formHeading && (typeof formHeading === "string" ? /* @__PURE__ */ jsxRuntime.jsx("h3", { className: cn("mb-6 text-2xl font-semibold tracking-tight", formHeadingClassName), children: formHeading }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: formHeadingClassName, children: formHeading })),
1559
+ formHeading && (typeof formHeading === "string" ? /* @__PURE__ */ jsxRuntime.jsx(
1560
+ "h3",
1561
+ {
1562
+ className: cn(
1563
+ "mb-6 text-2xl font-semibold tracking-tight",
1564
+ formHeadingClassName
1565
+ ),
1566
+ children: formHeading
1567
+ }
1568
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: formHeadingClassName, children: formHeading })),
1238
1569
  /* @__PURE__ */ jsxRuntime.jsxs(
1239
1570
  forms.Form,
1240
1571
  {
1241
1572
  form,
1242
1573
  action: formConfig?.endpoint,
1243
1574
  method: formMethod,
1575
+ submissionError,
1576
+ successMessage,
1577
+ successMessageClassName,
1578
+ errorMessageClassName,
1579
+ submissionConfig: formConfig?.submissionConfig,
1580
+ onNewSubmission: resetSubmissionState,
1244
1581
  className: cn("space-y-6", formClassName),
1245
1582
  children: [
1246
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
1247
- /* @__PURE__ */ jsxRuntime.jsx(forms.Field, { name: "firstName", children: ({ field, meta }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1248
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "first-name", children: "First Name" }),
1249
- /* @__PURE__ */ jsxRuntime.jsx(
1250
- TextInput,
1251
- {
1252
- ...field,
1253
- id: "first-name",
1254
- placeholder: "John",
1255
- error: meta.touched && !!meta.error,
1256
- "aria-label": "First Name"
1257
- }
1258
- )
1259
- ] }) }),
1260
- /* @__PURE__ */ jsxRuntime.jsx(forms.Field, { name: "lastName", children: ({ field, meta }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1261
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "last-name", children: "Last Name" }),
1262
- /* @__PURE__ */ jsxRuntime.jsx(
1263
- TextInput,
1264
- {
1265
- ...field,
1266
- id: "last-name",
1267
- placeholder: "Doe",
1268
- error: meta.touched && !!meta.error,
1269
- "aria-label": "Last Name"
1270
- }
1271
- )
1272
- ] }) })
1273
- ] }),
1274
- /* @__PURE__ */ jsxRuntime.jsx(forms.Field, { name: "email", children: ({ field, meta }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1275
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "email", children: "Email Address" }),
1276
- /* @__PURE__ */ jsxRuntime.jsx(
1277
- TextInput,
1278
- {
1279
- ...field,
1280
- id: "email",
1281
- type: "email",
1282
- placeholder: "john@example.com",
1283
- error: meta.touched && !!meta.error,
1284
- "aria-label": "Email Address"
1285
- }
1286
- )
1287
- ] }) }),
1288
- /* @__PURE__ */ jsxRuntime.jsx(forms.Field, { name: "message", children: ({ field, meta }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1289
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "message", children: "Message" }),
1290
- /* @__PURE__ */ jsxRuntime.jsx(
1291
- TextArea,
1292
- {
1293
- ...field,
1294
- id: "message",
1295
- placeholder: "Tell us how we can help...",
1296
- rows: 4,
1297
- error: meta.touched && !!meta.error,
1298
- "aria-label": "Message"
1299
- }
1300
- )
1301
- ] }) }),
1302
- /* @__PURE__ */ jsxRuntime.jsx(forms.Field, { name: "privacyPolicy", children: ({ field }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1303
- /* @__PURE__ */ jsxRuntime.jsx(
1304
- Checkbox,
1305
- {
1306
- id: "privacy-policy",
1307
- checked: field.value,
1308
- onCheckedChange: (checked) => field.onChange(checked === true)
1309
- }
1310
- ),
1311
- /* @__PURE__ */ jsxRuntime.jsxs(
1312
- Label,
1313
- {
1314
- htmlFor: "privacy-policy",
1315
- className: "cursor-pointer text-sm font-normal",
1316
- children: [
1317
- "I agree to the",
1318
- " ",
1319
- /* @__PURE__ */ jsxRuntime.jsx(Pressable, { href: "#", className: "text-primary hover:underline", children: "Privacy Policy" })
1320
- ]
1321
- }
1322
- )
1323
- ] }) }),
1583
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-12 gap-6", children: fields.map((field) => /* @__PURE__ */ jsxRuntime.jsx(
1584
+ "div",
1585
+ {
1586
+ className: getColumnSpanClass(field.columnSpan),
1587
+ children: /* @__PURE__ */ jsxRuntime.jsx(DynamicFormField, { field })
1588
+ },
1589
+ field.name
1590
+ )) }),
1324
1591
  actionsSlot || actions && actions.length > 0 ? actionsContent : /* @__PURE__ */ jsxRuntime.jsxs(
1325
1592
  Pressable,
1326
1593
  {
@@ -1340,8 +1607,17 @@ function ContactCard({
1340
1607
  )
1341
1608
  ] }),
1342
1609
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("lg:pt-8", infoPanelClassName), children: [
1343
- heading && (typeof heading === "string" ? /* @__PURE__ */ jsxRuntime.jsx("h2", { className: cn("mb-3 text-3xl font-bold tracking-tight", headingClassName), children: heading }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: headingClassName, children: heading })),
1344
- description && (typeof description === "string" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cn("leading-relaxed text-muted-foreground", descriptionClassName), children: description }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: descriptionClassName, children: description })),
1610
+ heading && (typeof heading === "string" ? /* @__PURE__ */ jsxRuntime.jsx(
1611
+ "h2",
1612
+ {
1613
+ className: cn(
1614
+ "mb-3 text-3xl font-bold tracking-tight",
1615
+ headingClassName
1616
+ ),
1617
+ children: heading
1618
+ }
1619
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: headingClassName, children: heading })),
1620
+ description && (typeof description === "string" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cn("leading-relaxed", descriptionClassName), children: description }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: descriptionClassName, children: description })),
1345
1621
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("mt-10 space-y-4", contactOptionsClassName), children: contactOptionsContent })
1346
1622
  ] })
1347
1623
  ] }) })