@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 { Select, 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 LabelPrimitive from '@radix-ui/react-label';
11
- import * as SeparatorPrimitive from '@radix-ui/react-separator';
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-consultation.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";
@@ -435,6 +432,231 @@ var Pressable = React.forwardRef(
435
432
  }
436
433
  );
437
434
  Pressable.displayName = "Pressable";
435
+ function Card({ className, ...props }) {
436
+ return /* @__PURE__ */ jsx(
437
+ "div",
438
+ {
439
+ "data-slot": "card",
440
+ className: cn(
441
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
442
+ className
443
+ ),
444
+ ...props
445
+ }
446
+ );
447
+ }
448
+ function CardContent({ className, ...props }) {
449
+ return /* @__PURE__ */ jsx(
450
+ "div",
451
+ {
452
+ "data-slot": "card-content",
453
+ className: cn("px-6", className),
454
+ ...props
455
+ }
456
+ );
457
+ }
458
+ function DynamicFormField({
459
+ field,
460
+ className,
461
+ uploadProgress = {},
462
+ onFileUpload,
463
+ onFileRemove,
464
+ isUploading = false
465
+ }) {
466
+ const fieldId = field.name;
467
+ const usesGroupLegend = field.type === "radio" || field.type === "checkbox-group";
468
+ const usesInlineCheckboxLabel = field.type === "checkbox";
469
+ const shouldRenderFieldLabel = !usesGroupLegend && !usesInlineCheckboxLabel;
470
+ const checkboxLabel = /* @__PURE__ */ jsxs(Fragment, { children: [
471
+ field.label,
472
+ field.required ? /* @__PURE__ */ jsx("span", { className: "text-destructive ml-1", children: "*" }) : null
473
+ ] });
474
+ return /* @__PURE__ */ jsx(
475
+ Field,
476
+ {
477
+ name: field.name,
478
+ label: shouldRenderFieldLabel ? field.label : void 0,
479
+ description: shouldRenderFieldLabel ? field.description : void 0,
480
+ required: field.required,
481
+ className: cn("space-y-2", className),
482
+ children: ({ field: formField, meta }) => /* @__PURE__ */ jsxs("div", { children: [
483
+ (field.type === "text" || field.type === "email" || field.type === "tel" || field.type === "search" || field.type === "password" || field.type === "url") && /* @__PURE__ */ jsx(
484
+ TextInput,
485
+ {
486
+ ...formField,
487
+ id: fieldId,
488
+ type: field.type,
489
+ placeholder: field.placeholder,
490
+ error: meta.touched && !!meta.error,
491
+ disabled: field.disabled,
492
+ "aria-label": field.label
493
+ }
494
+ ),
495
+ field.type === "number" && /* @__PURE__ */ jsx(
496
+ TextInput,
497
+ {
498
+ ...formField,
499
+ id: fieldId,
500
+ type: "text",
501
+ placeholder: field.placeholder,
502
+ error: meta.touched && !!meta.error,
503
+ disabled: field.disabled,
504
+ "aria-label": field.label
505
+ }
506
+ ),
507
+ field.type === "textarea" && /* @__PURE__ */ jsx(
508
+ TextArea,
509
+ {
510
+ ...formField,
511
+ id: fieldId,
512
+ placeholder: field.placeholder,
513
+ rows: field.rows || 4,
514
+ error: meta.touched && !!meta.error,
515
+ disabled: field.disabled,
516
+ "aria-label": field.label
517
+ }
518
+ ),
519
+ field.type === "select" && field.options && /* @__PURE__ */ jsx(
520
+ Select,
521
+ {
522
+ ...formField,
523
+ id: fieldId,
524
+ options: field.options,
525
+ placeholder: field.placeholder || `Select ${field.label.toLowerCase()}`,
526
+ error: meta.touched && !!meta.error,
527
+ disabled: field.disabled,
528
+ "aria-label": field.label
529
+ }
530
+ ),
531
+ field.type === "multi-select" && field.options && /* @__PURE__ */ jsx(
532
+ MultiSelect,
533
+ {
534
+ ...formField,
535
+ id: fieldId,
536
+ options: field.options,
537
+ placeholder: field.placeholder || `Select ${field.label.toLowerCase()}`,
538
+ error: meta.touched && !!meta.error,
539
+ disabled: field.disabled,
540
+ "aria-label": field.label
541
+ }
542
+ ),
543
+ field.type === "radio" && field.options && /* @__PURE__ */ jsx(
544
+ Radio,
545
+ {
546
+ ...formField,
547
+ id: fieldId,
548
+ options: field.options,
549
+ label: field.label,
550
+ description: field.description,
551
+ required: field.required,
552
+ disabled: field.disabled,
553
+ layout: field.layout || "stacked",
554
+ error: meta.touched && !!meta.error,
555
+ "aria-label": field.label
556
+ }
557
+ ),
558
+ field.type === "checkbox" && /* @__PURE__ */ jsx(
559
+ Checkbox,
560
+ {
561
+ ...formField,
562
+ id: fieldId,
563
+ value: formField.value === true || formField.value === "true",
564
+ onChange: (checked) => formField.onChange(checked),
565
+ label: checkboxLabel,
566
+ description: field.description,
567
+ disabled: field.disabled,
568
+ required: field.required,
569
+ error: meta.touched && !!meta.error,
570
+ "aria-label": field.label
571
+ }
572
+ ),
573
+ field.type === "checkbox-group" && field.options && /* @__PURE__ */ jsx(
574
+ CheckboxGroup,
575
+ {
576
+ ...formField,
577
+ id: fieldId,
578
+ options: field.options,
579
+ label: field.label,
580
+ description: field.description,
581
+ required: field.required,
582
+ disabled: field.disabled,
583
+ layout: field.layout || "stacked",
584
+ error: meta.touched && !!meta.error,
585
+ "aria-label": field.label
586
+ }
587
+ ),
588
+ (field.type === "date-picker" || field.type === "date") && /* @__PURE__ */ jsx(
589
+ DatePicker,
590
+ {
591
+ ...formField,
592
+ id: fieldId,
593
+ placeholder: field.placeholder,
594
+ error: meta.touched && !!meta.error,
595
+ disabled: field.disabled,
596
+ "aria-label": field.label
597
+ }
598
+ ),
599
+ field.type === "date-range" && /* @__PURE__ */ jsx(
600
+ DateRangePicker,
601
+ {
602
+ ...formField,
603
+ id: fieldId,
604
+ placeholder: field.placeholder,
605
+ error: meta.touched && !!meta.error,
606
+ disabled: field.disabled,
607
+ "aria-label": field.label
608
+ }
609
+ ),
610
+ field.type === "time" && /* @__PURE__ */ jsx(
611
+ TimePicker,
612
+ {
613
+ ...formField,
614
+ id: fieldId,
615
+ placeholder: field.placeholder,
616
+ error: meta.touched && !!meta.error,
617
+ disabled: field.disabled,
618
+ "aria-label": field.label
619
+ }
620
+ ),
621
+ field.type === "file" && /* @__PURE__ */ jsx(
622
+ FileInput,
623
+ {
624
+ ...formField,
625
+ id: fieldId,
626
+ accept: field.accept,
627
+ maxSize: field.maxSize || 5 * 1024 * 1024,
628
+ maxFiles: field.maxFiles || 1,
629
+ multiple: field.multiple || false,
630
+ placeholder: field.placeholder || "Choose file(s)...",
631
+ error: meta.touched && !!meta.error,
632
+ disabled: field.disabled || isUploading,
633
+ showProgress: true,
634
+ uploadProgress,
635
+ onChange: (files) => {
636
+ formField.onChange(files);
637
+ if (files.length > 0 && onFileUpload) {
638
+ onFileUpload(files);
639
+ }
640
+ },
641
+ onFileRemove,
642
+ "aria-label": field.label
643
+ }
644
+ ),
645
+ field.type === "rich-text" && /* @__PURE__ */ jsx(
646
+ RichTextEditor,
647
+ {
648
+ ...formField,
649
+ id: fieldId,
650
+ placeholder: field.placeholder,
651
+ error: meta.touched && !!meta.error,
652
+ disabled: field.disabled,
653
+ "aria-label": field.label
654
+ }
655
+ )
656
+ ] })
657
+ }
658
+ );
659
+ }
438
660
  var svgCache = /* @__PURE__ */ new Map();
439
661
  function DynamicIcon({
440
662
  name,
@@ -540,65 +762,169 @@ function processSvgForCurrentColor(svg) {
540
762
  );
541
763
  return processed;
542
764
  }
543
- function Card({ className, ...props }) {
544
- return /* @__PURE__ */ jsx(
545
- "div",
546
- {
547
- "data-slot": "card",
548
- className: cn(
549
- "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
550
- className
551
- ),
552
- ...props
553
- }
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
+ {}
554
784
  );
555
785
  }
556
- function CardContent({ className, ...props }) {
557
- return /* @__PURE__ */ jsx(
558
- "div",
559
- {
560
- "data-slot": "card-content",
561
- className: cn("px-6", className),
562
- ...props
563
- }
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
+ {}
564
815
  );
565
816
  }
566
- function Label({
567
- className,
568
- ...props
569
- }) {
570
- return /* @__PURE__ */ jsx(
571
- LabelPrimitive.Root,
572
- {
573
- "data-slot": "label",
574
- className: cn(
575
- "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",
576
- className
577
- ),
578
- ...props
579
- }
580
- );
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)}`;
581
820
  }
582
- function Separator({
583
- className,
584
- orientation = "horizontal",
585
- decorative = true,
586
- ...props
587
- }) {
588
- return /* @__PURE__ */ jsx(
589
- SeparatorPrimitive.Root,
590
- {
591
- "data-slot": "separator",
592
- decorative,
593
- orientation,
594
- className: cn(
595
- "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
596
- className
597
- ),
598
- ...props
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;
599
841
  }
600
- );
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
+ };
601
925
  }
926
+
927
+ // lib/forms.ts
602
928
  var PageSpeedFormSubmissionError = class extends Error {
603
929
  constructor(message, options = {}) {
604
930
  super(message);
@@ -1110,13 +1436,93 @@ var BUDGETS = [
1110
1436
  { value: "25k-50k", label: "$25,000 - $50,000" },
1111
1437
  { value: "50k-plus", label: "$50,000+" }
1112
1438
  ];
1439
+ var DEFAULT_FORM_FIELDS = [
1440
+ {
1441
+ name: "service",
1442
+ type: "select",
1443
+ label: "Service Needed",
1444
+ placeholder: "Select a service",
1445
+ required: true,
1446
+ columnSpan: 12,
1447
+ options: SERVICES
1448
+ },
1449
+ {
1450
+ name: "duration",
1451
+ type: "select",
1452
+ label: "Preferred Duration",
1453
+ placeholder: "Select duration",
1454
+ required: false,
1455
+ columnSpan: 6,
1456
+ options: DURATIONS
1457
+ },
1458
+ {
1459
+ name: "budget",
1460
+ type: "select",
1461
+ label: "Project Budget",
1462
+ placeholder: "Select budget range",
1463
+ required: false,
1464
+ columnSpan: 6,
1465
+ options: BUDGETS
1466
+ },
1467
+ {
1468
+ name: "firstName",
1469
+ type: "text",
1470
+ label: "First Name",
1471
+ placeholder: "John",
1472
+ required: true,
1473
+ columnSpan: 6
1474
+ },
1475
+ {
1476
+ name: "lastName",
1477
+ type: "text",
1478
+ label: "Last Name",
1479
+ placeholder: "Doe",
1480
+ required: true,
1481
+ columnSpan: 6
1482
+ },
1483
+ {
1484
+ name: "email",
1485
+ type: "email",
1486
+ label: "Email Address",
1487
+ placeholder: "john@example.com",
1488
+ required: true,
1489
+ columnSpan: 6
1490
+ },
1491
+ {
1492
+ name: "phone",
1493
+ type: "tel",
1494
+ label: "Phone Number",
1495
+ placeholder: "+1 (555) 000-0000",
1496
+ required: true,
1497
+ columnSpan: 6
1498
+ },
1499
+ {
1500
+ name: "company",
1501
+ type: "text",
1502
+ label: "Company Name",
1503
+ placeholder: "Acme Inc.",
1504
+ required: false,
1505
+ columnSpan: 12
1506
+ },
1507
+ {
1508
+ name: "details",
1509
+ type: "textarea",
1510
+ label: "Tell us about your needs",
1511
+ placeholder: "Describe your project, goals, and any specific challenges...",
1512
+ required: false,
1513
+ rows: 4,
1514
+ columnSpan: 12
1515
+ }
1516
+ ];
1113
1517
  function ContactConsultation({
1114
1518
  heading,
1115
1519
  description,
1116
- buttonText,
1520
+ buttonText = "Book Consultation",
1117
1521
  buttonIcon,
1118
1522
  actions,
1119
1523
  actionsSlot,
1524
+ formFields,
1525
+ successMessage = "Thank you for your consultation request! We'll be in touch within 24 hours to schedule your session.",
1120
1526
  className,
1121
1527
  headerClassName,
1122
1528
  headingClassName,
@@ -1124,6 +1530,8 @@ function ContactConsultation({
1124
1530
  cardClassName,
1125
1531
  cardContentClassName,
1126
1532
  formClassName,
1533
+ successMessageClassName,
1534
+ errorMessageClassName,
1127
1535
  submitClassName,
1128
1536
  spacing = "py-8 md:py-32",
1129
1537
  containerClassName = "px-6 sm:px-6 md:px-8 lg:px-8",
@@ -1135,60 +1543,17 @@ function ContactConsultation({
1135
1543
  onSuccess,
1136
1544
  onError
1137
1545
  }) {
1138
- const form = useForm({
1139
- initialValues: {
1140
- service: "",
1141
- duration: "60",
1142
- budget: "",
1143
- firstName: "",
1144
- lastName: "",
1145
- email: "",
1146
- phone: "",
1147
- company: "",
1148
- details: ""
1149
- },
1150
- validationSchema: {
1151
- service: (value) => !value ? "Please select a service" : void 0,
1152
- budget: (value) => !value ? "Please select a budget range" : void 0,
1153
- firstName: (value) => !value ? "First name is required" : void 0,
1154
- lastName: (value) => !value ? "Last name is required" : void 0,
1155
- email: (value) => {
1156
- if (!value) return "Email is required";
1157
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
1158
- return "Please enter a valid email address";
1159
- return void 0;
1160
- },
1161
- phone: (value) => !value ? "Phone number is required" : void 0
1162
- },
1163
- onSubmit: async (values, helpers) => {
1164
- const shouldAutoSubmit = Boolean(formConfig?.endpoint);
1165
- if (!shouldAutoSubmit && !onSubmit) {
1166
- return;
1167
- }
1168
- try {
1169
- let result;
1170
- if (shouldAutoSubmit) {
1171
- result = await submitPageSpeedForm(values, formConfig);
1172
- }
1173
- if (onSubmit) {
1174
- await onSubmit(values);
1175
- }
1176
- if (shouldAutoSubmit || onSubmit) {
1177
- if (formConfig?.resetOnSuccess !== false) {
1178
- helpers.resetForm();
1179
- }
1180
- onSuccess?.(result);
1181
- }
1182
- } catch (error) {
1183
- if (error instanceof PageSpeedFormSubmissionError && error.formErrors) {
1184
- helpers.setErrors(error.formErrors);
1185
- }
1186
- onError?.(error);
1187
- throw error;
1188
- }
1189
- }
1546
+ const fields = useMemo(
1547
+ () => formFields || DEFAULT_FORM_FIELDS,
1548
+ [formFields]
1549
+ );
1550
+ const { form, submissionError, formMethod, resetSubmissionState } = useContactForm({
1551
+ formFields: fields,
1552
+ formConfig,
1553
+ onSubmit,
1554
+ onSuccess,
1555
+ onError
1190
1556
  });
1191
- const formMethod = formConfig?.method?.toLowerCase() === "get" ? "get" : "post";
1192
1557
  const actionsContent = React.useMemo(() => {
1193
1558
  if (actionsSlot) return actionsSlot;
1194
1559
  if (actions && actions.length > 0) {
@@ -1257,147 +1622,15 @@ function ContactConsultation({
1257
1622
  form,
1258
1623
  action: formConfig?.endpoint,
1259
1624
  method: formMethod,
1260
- className: cn("space-y-8", formClassName),
1625
+ submissionError,
1626
+ successMessage,
1627
+ successMessageClassName,
1628
+ errorMessageClassName,
1629
+ submissionConfig: formConfig?.submissionConfig,
1630
+ onNewSubmission: resetSubmissionState,
1631
+ className: cn("space-y-6", formClassName),
1261
1632
  children: [
1262
- /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1263
- /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: "Consultation Details" }),
1264
- /* @__PURE__ */ jsx(Field, { name: "service", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1265
- /* @__PURE__ */ jsx(Label, { htmlFor: "service", children: "Service Type" }),
1266
- /* @__PURE__ */ jsxs(
1267
- Select,
1268
- {
1269
- ...field,
1270
- id: "service",
1271
- error: meta.touched && !!meta.error,
1272
- "aria-label": "Service Type",
1273
- children: [
1274
- /* @__PURE__ */ jsx("option", { value: "", children: "Select a service" }),
1275
- SERVICES.map((service) => /* @__PURE__ */ jsx("option", { value: service.value, children: service.label }, service.value))
1276
- ]
1277
- }
1278
- )
1279
- ] }) }),
1280
- /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
1281
- /* @__PURE__ */ jsx(Field, { name: "duration", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1282
- /* @__PURE__ */ jsx(Label, { htmlFor: "duration", children: "Preferred Duration" }),
1283
- /* @__PURE__ */ jsx(
1284
- Select,
1285
- {
1286
- ...field,
1287
- id: "duration",
1288
- error: meta.touched && !!meta.error,
1289
- "aria-label": "Preferred Duration",
1290
- children: DURATIONS.map((duration) => /* @__PURE__ */ jsx("option", { value: duration.value, children: duration.label }, duration.value))
1291
- }
1292
- )
1293
- ] }) }),
1294
- /* @__PURE__ */ jsx(Field, { name: "budget", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1295
- /* @__PURE__ */ jsx(Label, { htmlFor: "budget", children: "Budget Range" }),
1296
- /* @__PURE__ */ jsxs(
1297
- Select,
1298
- {
1299
- ...field,
1300
- id: "budget",
1301
- error: meta.touched && !!meta.error,
1302
- "aria-label": "Budget Range",
1303
- children: [
1304
- /* @__PURE__ */ jsx("option", { value: "", children: "Select budget range" }),
1305
- BUDGETS.map((budget) => /* @__PURE__ */ jsx("option", { value: budget.value, children: budget.label }, budget.value))
1306
- ]
1307
- }
1308
- )
1309
- ] }) })
1310
- ] })
1311
- ] }),
1312
- /* @__PURE__ */ jsx(Separator, {}),
1313
- /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1314
- /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: "Your Information" }),
1315
- /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
1316
- /* @__PURE__ */ jsx(Field, { name: "firstName", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1317
- /* @__PURE__ */ jsx(Label, { htmlFor: "first-name", children: "First Name" }),
1318
- /* @__PURE__ */ jsx(
1319
- TextInput,
1320
- {
1321
- ...field,
1322
- id: "first-name",
1323
- placeholder: "John",
1324
- error: meta.touched && !!meta.error,
1325
- "aria-label": "First Name"
1326
- }
1327
- )
1328
- ] }) }),
1329
- /* @__PURE__ */ jsx(Field, { name: "lastName", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1330
- /* @__PURE__ */ jsx(Label, { htmlFor: "last-name", children: "Last Name" }),
1331
- /* @__PURE__ */ jsx(
1332
- TextInput,
1333
- {
1334
- ...field,
1335
- id: "last-name",
1336
- placeholder: "Doe",
1337
- error: meta.touched && !!meta.error,
1338
- "aria-label": "Last Name"
1339
- }
1340
- )
1341
- ] }) })
1342
- ] }),
1343
- /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
1344
- /* @__PURE__ */ jsx(Field, { name: "email", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1345
- /* @__PURE__ */ jsx(Label, { htmlFor: "email", children: "Email" }),
1346
- /* @__PURE__ */ jsx(
1347
- TextInput,
1348
- {
1349
- ...field,
1350
- id: "email",
1351
- type: "email",
1352
- placeholder: "john@example.com",
1353
- error: meta.touched && !!meta.error,
1354
- "aria-label": "Email"
1355
- }
1356
- )
1357
- ] }) }),
1358
- /* @__PURE__ */ jsx(Field, { name: "phone", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1359
- /* @__PURE__ */ jsx(Label, { htmlFor: "phone", children: "Phone" }),
1360
- /* @__PURE__ */ jsx(
1361
- TextInput,
1362
- {
1363
- ...field,
1364
- id: "phone",
1365
- type: "tel",
1366
- placeholder: "+1 (555) 000-0000",
1367
- error: meta.touched && !!meta.error,
1368
- "aria-label": "Phone"
1369
- }
1370
- )
1371
- ] }) })
1372
- ] }),
1373
- /* @__PURE__ */ jsx(Field, { name: "company", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1374
- /* @__PURE__ */ jsx(Label, { htmlFor: "company", children: "Company (Optional)" }),
1375
- /* @__PURE__ */ jsx(
1376
- TextInput,
1377
- {
1378
- ...field,
1379
- id: "company",
1380
- placeholder: "Acme Inc.",
1381
- error: meta.touched && !!meta.error,
1382
- "aria-label": "Company"
1383
- }
1384
- )
1385
- ] }) }),
1386
- /* @__PURE__ */ jsx(Field, { name: "details", children: ({ field, meta }) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1387
- /* @__PURE__ */ jsx(Label, { htmlFor: "details", children: "Additional Details (Optional)" }),
1388
- /* @__PURE__ */ jsx(
1389
- TextArea,
1390
- {
1391
- ...field,
1392
- id: "details",
1393
- placeholder: "Tell us more about your needs...",
1394
- rows: 4,
1395
- error: meta.touched && !!meta.error,
1396
- "aria-label": "Additional Details"
1397
- }
1398
- )
1399
- ] }) })
1400
- ] }),
1633
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-12 gap-6", children: fields.map((field) => /* @__PURE__ */ jsx("div", { className: getColumnSpanClass(field.columnSpan), children: /* @__PURE__ */ jsx(DynamicFormField, { field }) }, field.name)) }),
1401
1634
  actionsSlot || actions && actions.length > 0 ? actionsContent : /* @__PURE__ */ jsxs(
1402
1635
  Pressable,
1403
1636
  {