@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,18 +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, Select, 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 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';
11
10
  import { serializeForRails, deserializeErrors } from '@page-speed/forms/integration';
12
11
 
13
12
  // components/blocks/contact/contact-callback.tsx
14
- var TextInput = TextInput$1;
15
- var TextArea = TextArea$1;
16
13
  function cn(...inputs) {
17
14
  return twMerge(clsx(inputs));
18
15
  }
@@ -359,6 +356,7 @@ var Pressable = React.forwardRef(
359
356
  rel,
360
357
  linkType,
361
358
  isInternal,
359
+ isExternal,
362
360
  handleClick
363
361
  } = navigation;
364
362
  const shouldRenderLink = normalizedHref && linkType !== "none";
@@ -562,22 +560,371 @@ function CardContent({ className, ...props }) {
562
560
  }
563
561
  );
564
562
  }
565
- function Label({
563
+ function DynamicFormField({
564
+ field,
566
565
  className,
567
- ...props
566
+ uploadProgress = {},
567
+ onFileUpload,
568
+ onFileRemove,
569
+ isUploading = false
568
570
  }) {
571
+ const fieldId = field.name;
572
+ const usesGroupLegend = field.type === "radio" || field.type === "checkbox-group";
573
+ const usesInlineCheckboxLabel = field.type === "checkbox";
574
+ const shouldRenderFieldLabel = !usesGroupLegend && !usesInlineCheckboxLabel;
575
+ const checkboxLabel = /* @__PURE__ */ jsxs(Fragment, { children: [
576
+ field.label,
577
+ field.required ? /* @__PURE__ */ jsx("span", { className: "text-destructive ml-1", children: "*" }) : null
578
+ ] });
569
579
  return /* @__PURE__ */ jsx(
570
- LabelPrimitive.Root,
580
+ Field,
571
581
  {
572
- "data-slot": "label",
573
- className: cn(
574
- "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",
575
- className
576
- ),
577
- ...props
582
+ name: field.name,
583
+ label: shouldRenderFieldLabel ? field.label : void 0,
584
+ description: shouldRenderFieldLabel ? field.description : void 0,
585
+ required: field.required,
586
+ className: cn("space-y-2", className),
587
+ children: ({ field: formField, meta }) => /* @__PURE__ */ jsxs("div", { children: [
588
+ (field.type === "text" || field.type === "email" || field.type === "tel" || field.type === "search" || field.type === "password" || field.type === "url") && /* @__PURE__ */ jsx(
589
+ TextInput,
590
+ {
591
+ ...formField,
592
+ id: fieldId,
593
+ type: field.type,
594
+ placeholder: field.placeholder,
595
+ error: meta.touched && !!meta.error,
596
+ disabled: field.disabled,
597
+ "aria-label": field.label
598
+ }
599
+ ),
600
+ field.type === "number" && /* @__PURE__ */ jsx(
601
+ TextInput,
602
+ {
603
+ ...formField,
604
+ id: fieldId,
605
+ type: "text",
606
+ placeholder: field.placeholder,
607
+ error: meta.touched && !!meta.error,
608
+ disabled: field.disabled,
609
+ "aria-label": field.label
610
+ }
611
+ ),
612
+ field.type === "textarea" && /* @__PURE__ */ jsx(
613
+ TextArea,
614
+ {
615
+ ...formField,
616
+ id: fieldId,
617
+ placeholder: field.placeholder,
618
+ rows: field.rows || 4,
619
+ error: meta.touched && !!meta.error,
620
+ disabled: field.disabled,
621
+ "aria-label": field.label
622
+ }
623
+ ),
624
+ field.type === "select" && field.options && /* @__PURE__ */ jsx(
625
+ Select,
626
+ {
627
+ ...formField,
628
+ id: fieldId,
629
+ options: field.options,
630
+ placeholder: field.placeholder || `Select ${field.label.toLowerCase()}`,
631
+ error: meta.touched && !!meta.error,
632
+ disabled: field.disabled,
633
+ "aria-label": field.label
634
+ }
635
+ ),
636
+ field.type === "multi-select" && field.options && /* @__PURE__ */ jsx(
637
+ MultiSelect,
638
+ {
639
+ ...formField,
640
+ id: fieldId,
641
+ options: field.options,
642
+ placeholder: field.placeholder || `Select ${field.label.toLowerCase()}`,
643
+ error: meta.touched && !!meta.error,
644
+ disabled: field.disabled,
645
+ "aria-label": field.label
646
+ }
647
+ ),
648
+ field.type === "radio" && field.options && /* @__PURE__ */ jsx(
649
+ Radio,
650
+ {
651
+ ...formField,
652
+ id: fieldId,
653
+ options: field.options,
654
+ label: field.label,
655
+ description: field.description,
656
+ required: field.required,
657
+ disabled: field.disabled,
658
+ layout: field.layout || "stacked",
659
+ error: meta.touched && !!meta.error,
660
+ "aria-label": field.label
661
+ }
662
+ ),
663
+ field.type === "checkbox" && /* @__PURE__ */ jsx(
664
+ Checkbox,
665
+ {
666
+ ...formField,
667
+ id: fieldId,
668
+ value: formField.value === true || formField.value === "true",
669
+ onChange: (checked) => formField.onChange(checked),
670
+ label: checkboxLabel,
671
+ description: field.description,
672
+ disabled: field.disabled,
673
+ required: field.required,
674
+ error: meta.touched && !!meta.error,
675
+ "aria-label": field.label
676
+ }
677
+ ),
678
+ field.type === "checkbox-group" && field.options && /* @__PURE__ */ jsx(
679
+ CheckboxGroup,
680
+ {
681
+ ...formField,
682
+ id: fieldId,
683
+ options: field.options,
684
+ label: field.label,
685
+ description: field.description,
686
+ required: field.required,
687
+ disabled: field.disabled,
688
+ layout: field.layout || "stacked",
689
+ error: meta.touched && !!meta.error,
690
+ "aria-label": field.label
691
+ }
692
+ ),
693
+ (field.type === "date-picker" || field.type === "date") && /* @__PURE__ */ jsx(
694
+ DatePicker,
695
+ {
696
+ ...formField,
697
+ id: fieldId,
698
+ placeholder: field.placeholder,
699
+ error: meta.touched && !!meta.error,
700
+ disabled: field.disabled,
701
+ "aria-label": field.label
702
+ }
703
+ ),
704
+ field.type === "date-range" && /* @__PURE__ */ jsx(
705
+ DateRangePicker,
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 === "time" && /* @__PURE__ */ jsx(
716
+ TimePicker,
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 === "file" && /* @__PURE__ */ jsx(
727
+ FileInput,
728
+ {
729
+ ...formField,
730
+ id: fieldId,
731
+ accept: field.accept,
732
+ maxSize: field.maxSize || 5 * 1024 * 1024,
733
+ maxFiles: field.maxFiles || 1,
734
+ multiple: field.multiple || false,
735
+ placeholder: field.placeholder || "Choose file(s)...",
736
+ error: meta.touched && !!meta.error,
737
+ disabled: field.disabled || isUploading,
738
+ showProgress: true,
739
+ uploadProgress,
740
+ onChange: (files) => {
741
+ formField.onChange(files);
742
+ if (files.length > 0 && onFileUpload) {
743
+ onFileUpload(files);
744
+ }
745
+ },
746
+ onFileRemove,
747
+ "aria-label": field.label
748
+ }
749
+ ),
750
+ field.type === "rich-text" && /* @__PURE__ */ jsx(
751
+ RichTextEditor,
752
+ {
753
+ ...formField,
754
+ id: fieldId,
755
+ placeholder: field.placeholder,
756
+ error: meta.touched && !!meta.error,
757
+ disabled: field.disabled,
758
+ "aria-label": field.label
759
+ }
760
+ )
761
+ ] })
578
762
  }
579
763
  );
580
764
  }
765
+
766
+ // lib/form-field-types.ts
767
+ function generateInitialValues(fields) {
768
+ return fields.reduce(
769
+ (acc, field) => {
770
+ if (field.type === "checkbox") {
771
+ acc[field.name] = false;
772
+ } else if (field.type === "checkbox-group" || field.type === "multi-select") {
773
+ acc[field.name] = [];
774
+ } else if (field.type === "file") {
775
+ acc[field.name] = [];
776
+ } else if (field.type === "date-range") {
777
+ acc[field.name] = { start: null, end: null };
778
+ } else {
779
+ acc[field.name] = "";
780
+ }
781
+ return acc;
782
+ },
783
+ {}
784
+ );
785
+ }
786
+ function generateValidationSchema(fields) {
787
+ return fields.reduce(
788
+ (acc, field) => {
789
+ acc[field.name] = (value, allValues) => {
790
+ if (field.required) {
791
+ if (!value || typeof value === "string" && !value.trim()) {
792
+ return `${field.label} is required`;
793
+ }
794
+ }
795
+ if (field.type === "email" && value) {
796
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
797
+ return "Please enter a valid email address";
798
+ }
799
+ }
800
+ if (field.type === "url" && value) {
801
+ try {
802
+ new URL(value);
803
+ } catch {
804
+ return "Please enter a valid URL";
805
+ }
806
+ }
807
+ if (field.validator) {
808
+ return field.validator(value, allValues);
809
+ }
810
+ return void 0;
811
+ };
812
+ return acc;
813
+ },
814
+ {}
815
+ );
816
+ }
817
+ function getColumnSpanClass(span) {
818
+ if (!span || span === 12) return "col-span-12";
819
+ return `col-span-12 sm:col-span-${Math.min(span, 12)}`;
820
+ }
821
+ function useContactForm(options) {
822
+ const {
823
+ formFields,
824
+ formConfig,
825
+ onSubmit,
826
+ onSuccess,
827
+ onError,
828
+ resetOnSuccess = true,
829
+ uploadTokens = []
830
+ } = options;
831
+ const [submissionError, setSubmissionError] = useState(null);
832
+ const submissionConfig = formConfig?.submissionConfig;
833
+ const redirectUrl = submissionConfig?.redirectUrl;
834
+ const redirectNavigation = useNavigation({ href: redirectUrl });
835
+ const resetSubmissionState = useCallback(() => {
836
+ setSubmissionError(null);
837
+ }, []);
838
+ const performRedirect = useCallback(() => {
839
+ if (!redirectUrl || typeof window === "undefined") {
840
+ return;
841
+ }
842
+ const navigate = () => {
843
+ if (redirectNavigation.shouldUseRouter && redirectNavigation.normalizedHref) {
844
+ const handler = window.__opensiteNavigationHandler;
845
+ if (typeof handler === "function") {
846
+ try {
847
+ const handled = handler(redirectNavigation.normalizedHref, void 0);
848
+ if (handled !== false) {
849
+ return;
850
+ }
851
+ } catch (error) {
852
+ console.error("Internal redirect handler failed:", error);
853
+ }
854
+ }
855
+ }
856
+ const destination = redirectNavigation.normalizedHref || redirectUrl;
857
+ window.location.assign(destination);
858
+ };
859
+ window.setTimeout(navigate, 150);
860
+ }, [redirectNavigation, redirectUrl]);
861
+ const form = useForm({
862
+ initialValues: useMemo(
863
+ () => generateInitialValues(formFields),
864
+ [formFields]
865
+ ),
866
+ validationSchema: useMemo(
867
+ () => generateValidationSchema(formFields),
868
+ [formFields]
869
+ ),
870
+ onSubmit: async (values, helpers) => {
871
+ resetSubmissionState();
872
+ const shouldAutoSubmit = Boolean(formConfig?.endpoint);
873
+ if (!shouldAutoSubmit && !onSubmit) {
874
+ return;
875
+ }
876
+ try {
877
+ let result;
878
+ const submissionValues = {
879
+ ...values,
880
+ ...uploadTokens.length > 0 && {
881
+ contact_form_upload_tokens: uploadTokens
882
+ }
883
+ };
884
+ if (shouldAutoSubmit) {
885
+ result = await submitPageSpeedForm(submissionValues, formConfig);
886
+ }
887
+ if (onSubmit) {
888
+ await onSubmit(submissionValues);
889
+ }
890
+ if (shouldAutoSubmit || onSubmit) {
891
+ try {
892
+ await submissionConfig?.handleFormSubmission?.({
893
+ formData: submissionValues,
894
+ responseData: result
895
+ });
896
+ } catch (callbackError) {
897
+ console.error("handleFormSubmission callback failed:", callbackError);
898
+ }
899
+ if (resetOnSuccess) {
900
+ helpers.resetForm();
901
+ }
902
+ onSuccess?.(result);
903
+ if (submissionConfig?.behavior === "redirect" && submissionConfig.redirectUrl) {
904
+ performRedirect();
905
+ }
906
+ }
907
+ } catch (error) {
908
+ if (error instanceof PageSpeedFormSubmissionError && error.formErrors) {
909
+ helpers.setErrors(error.formErrors);
910
+ }
911
+ const errorMessage = error instanceof Error ? error.message : "Form submission failed";
912
+ setSubmissionError(errorMessage);
913
+ onError?.(error);
914
+ }
915
+ }
916
+ });
917
+ const formMethod = formConfig?.method?.toLowerCase() === "get" ? "get" : "post";
918
+ return {
919
+ form,
920
+ isSubmitted: form.status === "success",
921
+ submissionError,
922
+ formMethod,
923
+ resetSubmissionState
924
+ };
925
+ }
926
+
927
+ // lib/forms.ts
581
928
  var PageSpeedFormSubmissionError = class extends Error {
582
929
  constructor(message, options = {}) {
583
930
  super(message);
@@ -1068,41 +1415,121 @@ var Section = React__default.forwardRef(
1068
1415
  }
1069
1416
  );
1070
1417
  Section.displayName = "Section";
1071
- var TIME_SLOTS = [
1072
- "9:00 AM",
1073
- "10:00 AM",
1074
- "11:00 AM",
1075
- "12:00 PM",
1076
- "1:00 PM",
1077
- "2:00 PM",
1078
- "3:00 PM",
1079
- "4:00 PM",
1080
- "5:00 PM"
1081
- ];
1082
- var TOPICS = [
1083
- "Product Demo",
1084
- "Sales Inquiry",
1085
- "Technical Support",
1086
- "Partnership",
1087
- "General Question"
1088
- ];
1089
- var TIMEZONES = [
1090
- { value: "est", label: "Eastern Time (EST)" },
1091
- { value: "cst", label: "Central Time (CST)" },
1092
- { value: "mst", label: "Mountain Time (MST)" },
1093
- { value: "pst", label: "Pacific Time (PST)" }
1418
+ var DEFAULT_FORM_FIELDS = [
1419
+ {
1420
+ name: "name",
1421
+ type: "text",
1422
+ label: "Full Name",
1423
+ placeholder: "John Doe",
1424
+ required: true,
1425
+ columnSpan: 6
1426
+ },
1427
+ {
1428
+ name: "company",
1429
+ type: "text",
1430
+ label: "Company",
1431
+ placeholder: "Acme Inc.",
1432
+ required: false,
1433
+ columnSpan: 6
1434
+ },
1435
+ {
1436
+ name: "email",
1437
+ type: "email",
1438
+ label: "Email Address",
1439
+ placeholder: "john@example.com",
1440
+ required: true,
1441
+ columnSpan: 6
1442
+ },
1443
+ {
1444
+ name: "phone",
1445
+ type: "tel",
1446
+ label: "Phone Number",
1447
+ placeholder: "+1 (555) 000-0000",
1448
+ required: true,
1449
+ columnSpan: 6
1450
+ },
1451
+ {
1452
+ name: "date",
1453
+ type: "date",
1454
+ label: "Preferred Date",
1455
+ placeholder: "",
1456
+ required: true,
1457
+ columnSpan: 6
1458
+ },
1459
+ {
1460
+ name: "time",
1461
+ type: "select",
1462
+ label: "Preferred Time",
1463
+ placeholder: "Select a time",
1464
+ required: true,
1465
+ columnSpan: 6,
1466
+ options: [
1467
+ { value: "9:00 AM", label: "9:00 AM" },
1468
+ { value: "10:00 AM", label: "10:00 AM" },
1469
+ { value: "11:00 AM", label: "11:00 AM" },
1470
+ { value: "12:00 PM", label: "12:00 PM" },
1471
+ { value: "1:00 PM", label: "1:00 PM" },
1472
+ { value: "2:00 PM", label: "2:00 PM" },
1473
+ { value: "3:00 PM", label: "3:00 PM" },
1474
+ { value: "4:00 PM", label: "4:00 PM" },
1475
+ { value: "5:00 PM", label: "5:00 PM" }
1476
+ ]
1477
+ },
1478
+ {
1479
+ name: "timezone",
1480
+ type: "select",
1481
+ label: "Timezone",
1482
+ placeholder: "",
1483
+ required: true,
1484
+ columnSpan: 12,
1485
+ options: [
1486
+ { value: "est", label: "Eastern Time (EST)" },
1487
+ { value: "cst", label: "Central Time (CST)" },
1488
+ { value: "mst", label: "Mountain Time (MST)" },
1489
+ { value: "pst", label: "Pacific Time (PST)" }
1490
+ ]
1491
+ },
1492
+ {
1493
+ name: "topic",
1494
+ type: "select",
1495
+ label: "Topic",
1496
+ placeholder: "Select a topic",
1497
+ required: true,
1498
+ columnSpan: 12,
1499
+ options: [
1500
+ { value: "product-demo", label: "Product Demo" },
1501
+ { value: "sales-inquiry", label: "Sales Inquiry" },
1502
+ { value: "technical-support", label: "Technical Support" },
1503
+ { value: "partnership", label: "Partnership" },
1504
+ { value: "general-question", label: "General Question" }
1505
+ ]
1506
+ },
1507
+ {
1508
+ name: "details",
1509
+ type: "textarea",
1510
+ label: "Additional Details (Optional)",
1511
+ placeholder: "Help us prepare for the call by sharing any specific questions or topics you'd like to cover...",
1512
+ required: false,
1513
+ rows: 4,
1514
+ columnSpan: 12
1515
+ }
1094
1516
  ];
1095
1517
  function ContactCallback({
1096
1518
  heading,
1097
1519
  description,
1098
- buttonText,
1520
+ buttonText = "Schedule Callback",
1099
1521
  buttonIcon = /* @__PURE__ */ jsx(DynamicIcon, { name: "lucide/phone", size: 16 }),
1100
1522
  actions,
1101
1523
  actionsSlot,
1102
- footer,
1103
- footerSlot,
1524
+ formFields,
1525
+ successMessage = "Thank you! Your callback request has been received.",
1526
+ infoSectionLabel,
1527
+ scheduleSectionLabel,
1528
+ topicSectionLabel,
1529
+ callbackProcessLabel,
1530
+ callbackProcessDescription,
1104
1531
  className,
1105
- containerClassName,
1532
+ containerClassName = "px-6 sm:px-6 md:px-8 lg:px-8",
1106
1533
  headerClassName,
1107
1534
  headingClassName,
1108
1535
  descriptionClassName,
@@ -1110,75 +1537,40 @@ function ContactCallback({
1110
1537
  cardContentClassName,
1111
1538
  formClassName,
1112
1539
  submitClassName,
1113
- footerClassName,
1114
- background = "white",
1115
- spacing = "xl",
1540
+ successMessageClassName,
1541
+ errorMessageClassName,
1542
+ background,
1543
+ spacing = "py-8 md:py-32",
1116
1544
  pattern,
1117
- patternOpacity = 0.1,
1545
+ patternOpacity,
1118
1546
  formConfig,
1119
1547
  onSubmit,
1120
1548
  onSuccess,
1121
1549
  onError
1122
1550
  }) {
1123
- const form = useForm({
1124
- initialValues: {
1125
- name: "",
1126
- company: "",
1127
- email: "",
1128
- phone: "",
1129
- date: "",
1130
- time: "",
1131
- timezone: "est",
1132
- topic: "",
1133
- details: ""
1134
- },
1135
- validationSchema: {
1136
- name: (value) => !value ? "Name is required" : void 0,
1137
- email: (value) => {
1138
- if (!value) return "Email is required";
1139
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
1140
- return "Please enter a valid email address";
1141
- return void 0;
1142
- },
1143
- phone: (value) => !value ? "Phone number is required" : void 0,
1144
- date: (value) => !value ? "Date is required" : void 0,
1145
- time: (value) => !value ? "Time is required" : void 0,
1146
- topic: (value) => !value ? "Topic is required" : void 0
1147
- },
1148
- onSubmit: async (values, helpers) => {
1149
- const shouldAutoSubmit = Boolean(formConfig?.endpoint);
1150
- if (!shouldAutoSubmit && !onSubmit) {
1151
- return;
1152
- }
1153
- try {
1154
- let result;
1155
- if (shouldAutoSubmit) {
1156
- result = await submitPageSpeedForm(values, formConfig);
1157
- }
1158
- if (onSubmit) {
1159
- await onSubmit(values);
1160
- }
1161
- if (shouldAutoSubmit || onSubmit) {
1162
- if (formConfig?.resetOnSuccess !== false) {
1163
- helpers.resetForm();
1164
- }
1165
- onSuccess?.(result);
1166
- }
1167
- } catch (error) {
1168
- if (error instanceof PageSpeedFormSubmissionError && error.formErrors) {
1169
- helpers.setErrors(error.formErrors);
1170
- }
1171
- onError?.(error);
1172
- throw error;
1173
- }
1174
- }
1551
+ const fields = useMemo(
1552
+ () => formFields || DEFAULT_FORM_FIELDS,
1553
+ [formFields]
1554
+ );
1555
+ const { form, submissionError, formMethod, resetSubmissionState } = useContactForm({
1556
+ formFields: fields,
1557
+ formConfig,
1558
+ onSubmit,
1559
+ onSuccess,
1560
+ onError
1175
1561
  });
1176
- const formMethod = formConfig?.method?.toLowerCase() === "get" ? "get" : "post";
1177
- const actionsContent = React.useMemo(() => {
1562
+ const actionsContent = useMemo(() => {
1178
1563
  if (actionsSlot) return actionsSlot;
1179
1564
  if (actions && actions.length > 0) {
1180
1565
  return actions.map((action, index) => {
1181
- const { label, icon, iconAfter, children, className: actionClassName, ...pressableProps } = action;
1566
+ const {
1567
+ label,
1568
+ icon,
1569
+ iconAfter,
1570
+ children,
1571
+ className: actionClassName,
1572
+ ...pressableProps
1573
+ } = action;
1182
1574
  return /* @__PURE__ */ jsx(
1183
1575
  Pressable,
1184
1576
  {
@@ -1197,13 +1589,6 @@ function ContactCallback({
1197
1589
  }
1198
1590
  return null;
1199
1591
  }, [actionsSlot, actions]);
1200
- const footerContent = React.useMemo(() => {
1201
- if (footerSlot) return footerSlot;
1202
- if (footer) {
1203
- return typeof footer === "string" ? /* @__PURE__ */ jsx("p", { className: cn("mt-6 text-center text-sm text-muted-foreground", footerClassName), children: footer }) : /* @__PURE__ */ jsx("div", { className: cn("mt-6 text-center text-sm text-muted-foreground", footerClassName), children: footer });
1204
- }
1205
- return null;
1206
- }, [footerSlot, footer, footerClassName]);
1207
1592
  return /* @__PURE__ */ jsx(
1208
1593
  Section,
1209
1594
  {
@@ -1211,11 +1596,21 @@ function ContactCallback({
1211
1596
  spacing,
1212
1597
  pattern,
1213
1598
  patternOpacity,
1214
- className: cn("pb-12", className),
1215
- children: /* @__PURE__ */ jsxs("div", { className: cn("mx-auto max-w-4xl px-4", containerClassName), children: [
1216
- /* @__PURE__ */ jsxs("div", { className: cn("mb-10 text-center", headerClassName), children: [
1217
- 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 })),
1218
- description && (typeof description === "string" ? /* @__PURE__ */ jsx("p", { className: cn("leading-relaxed text-muted-foreground", descriptionClassName), children: description }) : /* @__PURE__ */ jsx("div", { className: descriptionClassName, children: description }))
1599
+ className,
1600
+ containerClassName,
1601
+ children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1602
+ (heading || description) && /* @__PURE__ */ jsxs("div", { className: cn("mb-10 text-center", headerClassName), children: [
1603
+ heading && (typeof heading === "string" ? /* @__PURE__ */ jsx(
1604
+ "h2",
1605
+ {
1606
+ className: cn(
1607
+ "mb-3 text-3xl font-bold tracking-tight",
1608
+ headingClassName
1609
+ ),
1610
+ children: heading
1611
+ }
1612
+ ) : /* @__PURE__ */ jsx("div", { className: headingClassName, children: heading })),
1613
+ description && (typeof description === "string" ? /* @__PURE__ */ jsx("p", { className: cn("leading-relaxed", descriptionClassName), children: description }) : /* @__PURE__ */ jsx("div", { className: descriptionClassName, children: description }))
1219
1614
  ] }),
1220
1615
  /* @__PURE__ */ jsx(Card, { className: cardClassName, children: /* @__PURE__ */ jsx(CardContent, { className: cn("p-6 lg:p-8", cardContentClassName), children: /* @__PURE__ */ jsxs(
1221
1616
  Form,
@@ -1223,174 +1618,34 @@ function ContactCallback({
1223
1618
  form,
1224
1619
  action: formConfig?.endpoint,
1225
1620
  method: formMethod,
1621
+ submissionError,
1622
+ successMessage,
1623
+ successMessageClassName,
1624
+ errorMessageClassName,
1625
+ submissionConfig: formConfig?.submissionConfig,
1626
+ onNewSubmission: resetSubmissionState,
1226
1627
  className: cn("space-y-6", formClassName),
1227
1628
  children: [
1228
- /* @__PURE__ */ jsxs("div", { children: [
1229
- /* @__PURE__ */ jsx("h3", { className: "mb-4 text-lg font-semibold", children: "Your Information" }),
1230
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-6 sm:grid-cols-2", children: [
1231
- /* @__PURE__ */ jsx(Field, { name: "name", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1232
- /* @__PURE__ */ jsx(Label, { htmlFor: "name", children: "Full Name" }),
1233
- /* @__PURE__ */ jsx(
1234
- TextInput,
1235
- {
1236
- ...field,
1237
- id: "name",
1238
- placeholder: "John Doe",
1239
- error: meta.touched && !!meta.error,
1240
- "aria-label": "Full Name"
1241
- }
1242
- )
1243
- ] }) }),
1244
- /* @__PURE__ */ jsx(Field, { name: "company", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1245
- /* @__PURE__ */ jsx(Label, { htmlFor: "company", children: "Company" }),
1246
- /* @__PURE__ */ jsx(
1247
- TextInput,
1248
- {
1249
- ...field,
1250
- id: "company",
1251
- placeholder: "Acme Inc.",
1252
- error: meta.touched && !!meta.error,
1253
- "aria-label": "Company"
1254
- }
1255
- )
1256
- ] }) })
1257
- ] })
1258
- ] }),
1259
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-6 sm:grid-cols-2", children: [
1260
- /* @__PURE__ */ jsx(Field, { name: "email", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1261
- /* @__PURE__ */ jsx(Label, { htmlFor: "email", children: "Email Address" }),
1262
- /* @__PURE__ */ jsx(
1263
- TextInput,
1264
- {
1265
- ...field,
1266
- id: "email",
1267
- type: "email",
1268
- placeholder: "john@example.com",
1269
- error: meta.touched && !!meta.error,
1270
- "aria-label": "Email Address"
1271
- }
1272
- )
1273
- ] }) }),
1274
- /* @__PURE__ */ jsx(Field, { name: "phone", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1275
- /* @__PURE__ */ jsx(Label, { htmlFor: "phone", children: "Phone Number" }),
1276
- /* @__PURE__ */ jsx(
1277
- TextInput,
1278
- {
1279
- ...field,
1280
- id: "phone",
1281
- type: "tel",
1282
- placeholder: "+1 (555) 000-0000",
1283
- error: meta.touched && !!meta.error,
1284
- "aria-label": "Phone Number"
1285
- }
1286
- )
1287
- ] }) })
1288
- ] }),
1289
- /* @__PURE__ */ jsxs("div", { className: "border-t pt-6", children: [
1290
- /* @__PURE__ */ jsx("h3", { className: "mb-4 text-lg font-semibold", children: "Preferred Callback Time" }),
1291
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-6 sm:grid-cols-2", children: [
1292
- /* @__PURE__ */ jsx(Field, { name: "date", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1293
- /* @__PURE__ */ jsx(Label, { htmlFor: "date", children: "Preferred Date" }),
1294
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1295
- /* @__PURE__ */ jsx(
1296
- TextInput,
1297
- {
1298
- ...field,
1299
- id: "date",
1300
- type: "date",
1301
- className: "pl-10",
1302
- error: meta.touched && !!meta.error,
1303
- "aria-label": "Preferred Date"
1304
- }
1305
- ),
1306
- /* @__PURE__ */ jsx(
1307
- DynamicIcon,
1308
- {
1309
- name: "lucide/calendar",
1310
- size: 20,
1311
- className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
1312
- }
1313
- )
1314
- ] })
1315
- ] }) }),
1316
- /* @__PURE__ */ jsx(Field, { name: "time", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1317
- /* @__PURE__ */ jsx(Label, { htmlFor: "time", children: "Preferred Time" }),
1318
- /* @__PURE__ */ jsxs(
1319
- Select,
1320
- {
1321
- ...field,
1322
- id: "time",
1323
- error: meta.touched && !!meta.error,
1324
- "aria-label": "Preferred Time",
1325
- children: [
1326
- /* @__PURE__ */ jsx("option", { value: "", children: "Select a time" }),
1327
- TIME_SLOTS.map((slot) => /* @__PURE__ */ jsx("option", { value: slot, children: slot }, slot))
1328
- ]
1329
- }
1330
- )
1331
- ] }) })
1332
- ] }),
1333
- /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(Field, { name: "timezone", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1334
- /* @__PURE__ */ jsx(Label, { htmlFor: "timezone", children: "Timezone" }),
1335
- /* @__PURE__ */ jsx(
1336
- Select,
1337
- {
1338
- ...field,
1339
- id: "timezone",
1340
- error: meta.touched && !!meta.error,
1341
- "aria-label": "Timezone",
1342
- children: TIMEZONES.map((tz) => /* @__PURE__ */ jsx("option", { value: tz.value, children: tz.label }, tz.value))
1343
- }
1344
- )
1345
- ] }) }) })
1346
- ] }),
1347
- /* @__PURE__ */ jsxs("div", { className: "border-t pt-6", children: [
1348
- /* @__PURE__ */ jsx("h3", { className: "mb-4 text-lg font-semibold", children: "What would you like to discuss?" }),
1349
- /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1350
- /* @__PURE__ */ jsx(Field, { name: "topic", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1351
- /* @__PURE__ */ jsx(Label, { htmlFor: "topic", children: "Topic" }),
1352
- /* @__PURE__ */ jsxs(
1353
- Select,
1354
- {
1355
- ...field,
1356
- id: "topic",
1357
- error: meta.touched && !!meta.error,
1358
- "aria-label": "Topic",
1359
- children: [
1360
- /* @__PURE__ */ jsx("option", { value: "", children: "Select a topic" }),
1361
- TOPICS.map((topic) => /* @__PURE__ */ jsx("option", { value: topic.toLowerCase(), children: topic }, topic))
1362
- ]
1363
- }
1364
- )
1365
- ] }) }),
1366
- /* @__PURE__ */ jsx(Field, { name: "details", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1367
- /* @__PURE__ */ jsx(Label, { htmlFor: "details", children: "Additional Details (Optional)" }),
1368
- /* @__PURE__ */ jsx(
1369
- TextArea,
1370
- {
1371
- ...field,
1372
- id: "details",
1373
- placeholder: "Help us prepare for the call by sharing any specific questions or topics you'd like to cover...",
1374
- rows: 4,
1375
- error: meta.touched && !!meta.error,
1376
- "aria-label": "Additional Details"
1377
- }
1378
- )
1379
- ] }) })
1380
- ] })
1381
- ] }),
1382
- /* @__PURE__ */ jsx("div", { className: "rounded-lg border p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
1629
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-12 gap-6", children: fields.map((field) => /* @__PURE__ */ jsx(
1630
+ "div",
1631
+ {
1632
+ className: getColumnSpanClass(field.columnSpan),
1633
+ children: /* @__PURE__ */ jsx(DynamicFormField, { field })
1634
+ },
1635
+ field.name
1636
+ )) }),
1637
+ (callbackProcessLabel || callbackProcessDescription) && /* @__PURE__ */ jsx("div", { className: "rounded-lg border p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
1383
1638
  /* @__PURE__ */ jsx(
1384
1639
  DynamicIcon,
1385
1640
  {
1386
1641
  name: "lucide/clock",
1387
1642
  size: 20,
1388
- className: "mt-1 shrink-0 text-primary"
1643
+ className: "mt-1 shrink-0"
1389
1644
  }
1390
1645
  ),
1391
1646
  /* @__PURE__ */ jsxs("div", { className: "text-sm", children: [
1392
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: "Callback Process" }),
1393
- /* @__PURE__ */ jsx("p", { className: "mt-1 leading-relaxed text-muted-foreground", children: "We'll call you at the scheduled time at the phone number you provided. Please ensure you're available to answer. If you miss the call, we'll send you a follow-up email." })
1647
+ callbackProcessLabel && /* @__PURE__ */ jsx("p", { className: "font-medium", children: callbackProcessLabel }),
1648
+ callbackProcessDescription && /* @__PURE__ */ jsx("p", { className: "mt-1 leading-relaxed", children: callbackProcessDescription })
1394
1649
  ] })
1395
1650
  ] }) }),
1396
1651
  actionsSlot || actions && actions.length > 0 ? actionsContent : /* @__PURE__ */ jsxs(
@@ -1404,14 +1659,13 @@ function ContactCallback({
1404
1659
  disabled: form.isSubmitting,
1405
1660
  children: [
1406
1661
  buttonIcon,
1407
- buttonText
1662
+ buttonText || "Schedule Callback"
1408
1663
  ]
1409
1664
  }
1410
1665
  )
1411
1666
  ]
1412
1667
  }
1413
- ) }) }),
1414
- footerContent
1668
+ ) }) })
1415
1669
  ] })
1416
1670
  }
1417
1671
  );