@flamingo-stack/openframe-frontend-core 0.0.596-2034.5684.1 → 0.0.596-2034.5686.1

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 (132) hide show
  1. package/dist/__tests__/fixtures/meeting-booking.d.ts +21 -0
  2. package/dist/__tests__/fixtures/meeting-booking.d.ts.map +1 -1
  3. package/dist/{chunk-NE3SHUIJ.cjs → chunk-25LZZY6E.cjs} +3 -3
  4. package/dist/{chunk-NE3SHUIJ.cjs.map → chunk-25LZZY6E.cjs.map} +1 -1
  5. package/dist/{chunk-CBR4H6JJ.cjs → chunk-3IT35YDV.cjs} +53 -53
  6. package/dist/{chunk-CBR4H6JJ.cjs.map → chunk-3IT35YDV.cjs.map} +1 -1
  7. package/dist/{chunk-WTK5YZYU.js → chunk-A6YERC4N.js} +2 -2
  8. package/dist/{chunk-XNZMKCA4.js → chunk-AGKFT4FA.js} +5 -5
  9. package/dist/{chunk-QAJXIOK2.js → chunk-AYMBMVOD.js} +2 -2
  10. package/dist/{chunk-7XHONLYQ.js → chunk-BLYQD3PG.js} +2 -2
  11. package/dist/{chunk-WDOERQZK.cjs → chunk-D3Q3VYRU.cjs} +16 -16
  12. package/dist/{chunk-WDOERQZK.cjs.map → chunk-D3Q3VYRU.cjs.map} +1 -1
  13. package/dist/{chunk-DD64DQHM.cjs → chunk-E4YWJY3A.cjs} +4 -4
  14. package/dist/{chunk-DD64DQHM.cjs.map → chunk-E4YWJY3A.cjs.map} +1 -1
  15. package/dist/{chunk-E7FDTOJ6.js → chunk-FGHJ3BC7.js} +3 -3
  16. package/dist/{chunk-E6NN7MAN.js → chunk-FIQIT6HX.js} +2 -2
  17. package/dist/{chunk-J2OCCRFC.js → chunk-GZ76RASF.js} +4 -4
  18. package/dist/{chunk-BC73KAXX.cjs → chunk-HPUD7X67.cjs} +4 -4
  19. package/dist/{chunk-BC73KAXX.cjs.map → chunk-HPUD7X67.cjs.map} +1 -1
  20. package/dist/{chunk-B3446RB6.cjs → chunk-IDDGXYES.cjs} +17 -17
  21. package/dist/{chunk-B3446RB6.cjs.map → chunk-IDDGXYES.cjs.map} +1 -1
  22. package/dist/{chunk-ESHCKTES.js → chunk-J5EA7PMY.js} +4 -4
  23. package/dist/{chunk-3PUXHFC4.js → chunk-JVYNMMNE.js} +3 -3
  24. package/dist/{chunk-QT6Q6SUK.js → chunk-KOLVBEW5.js} +1 -1
  25. package/dist/{chunk-QT6Q6SUK.js.map → chunk-KOLVBEW5.js.map} +1 -1
  26. package/dist/{chunk-NBJELHZE.cjs → chunk-MN4TFBRA.cjs} +3 -3
  27. package/dist/{chunk-NBJELHZE.cjs.map → chunk-MN4TFBRA.cjs.map} +1 -1
  28. package/dist/{chunk-C2UBAAGD.cjs → chunk-N6HNK25A.cjs} +1 -1
  29. package/dist/{chunk-C2UBAAGD.cjs.map → chunk-N6HNK25A.cjs.map} +1 -1
  30. package/dist/{chunk-NTJGRZ7G.cjs → chunk-OI7RNQAS.cjs} +33 -33
  31. package/dist/{chunk-NTJGRZ7G.cjs.map → chunk-OI7RNQAS.cjs.map} +1 -1
  32. package/dist/{chunk-OKCGXXEK.js → chunk-PO4ATWE4.js} +2 -2
  33. package/dist/{chunk-X6TLDYMG.js → chunk-QNZM43HU.js} +3 -3
  34. package/dist/{chunk-6RISTRR3.cjs → chunk-QVOGALQE.cjs} +3 -3
  35. package/dist/{chunk-6RISTRR3.cjs.map → chunk-QVOGALQE.cjs.map} +1 -1
  36. package/dist/{chunk-IJ4SGRAN.js → chunk-S7LV66HD.js} +6 -6
  37. package/dist/{chunk-6X63WX5D.cjs → chunk-SLIUM3VO.cjs} +34 -34
  38. package/dist/{chunk-6X63WX5D.cjs.map → chunk-SLIUM3VO.cjs.map} +1 -1
  39. package/dist/{chunk-XTZECFMT.cjs → chunk-TDLETTI4.cjs} +10 -10
  40. package/dist/{chunk-XTZECFMT.cjs.map → chunk-TDLETTI4.cjs.map} +1 -1
  41. package/dist/{chunk-S2WP7ELW.js → chunk-TYURDSIN.js} +34 -32
  42. package/dist/chunk-TYURDSIN.js.map +1 -0
  43. package/dist/{chunk-DR4WXTKP.cjs → chunk-TZK5N6AH.cjs} +83 -81
  44. package/dist/chunk-TZK5N6AH.cjs.map +1 -0
  45. package/dist/{chunk-EM3T7YXE.js → chunk-Y6JB7X5Y.js} +2 -2
  46. package/dist/{chunk-HCHTKE6I.cjs → chunk-YP56SX7C.cjs} +3 -3
  47. package/dist/{chunk-HCHTKE6I.cjs.map → chunk-YP56SX7C.cjs.map} +1 -1
  48. package/dist/{chunk-UG43WQ2C.cjs → chunk-YQYOEGAT.cjs} +23 -23
  49. package/dist/{chunk-UG43WQ2C.cjs.map → chunk-YQYOEGAT.cjs.map} +1 -1
  50. package/dist/{chunk-YZALGXOM.js → chunk-ZVBYWM5Z.js} +2 -2
  51. package/dist/{chunk-ANGSMT5Q.cjs → chunk-ZXEYS2XP.cjs} +17 -17
  52. package/dist/{chunk-ANGSMT5Q.cjs.map → chunk-ZXEYS2XP.cjs.map} +1 -1
  53. package/dist/components/case-studies/index.cjs +9 -9
  54. package/dist/components/case-studies/index.js +3 -3
  55. package/dist/components/contact/index.cjs +4 -4
  56. package/dist/components/contact/index.js +3 -3
  57. package/dist/components/docs/index.cjs +7 -7
  58. package/dist/components/docs/index.js +6 -6
  59. package/dist/components/embeds/index.cjs +4 -4
  60. package/dist/components/embeds/index.js +3 -3
  61. package/dist/components/faq/index.cjs +4 -4
  62. package/dist/components/faq/index.js +3 -3
  63. package/dist/components/features/index.cjs +3 -3
  64. package/dist/components/features/index.js +2 -2
  65. package/dist/components/help-center-pages/index.cjs +23 -23
  66. package/dist/components/help-center-pages/index.js +14 -14
  67. package/dist/components/index.cjs +35 -33
  68. package/dist/components/index.cjs.map +1 -1
  69. package/dist/components/index.js +15 -13
  70. package/dist/components/index.js.map +1 -1
  71. package/dist/components/meeting-scheduler/booking-form.d.ts +15 -3
  72. package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
  73. package/dist/components/meeting-scheduler/index.cjs +81 -89
  74. package/dist/components/meeting-scheduler/index.cjs.map +1 -1
  75. package/dist/components/meeting-scheduler/index.d.ts +2 -0
  76. package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
  77. package/dist/components/meeting-scheduler/index.js +56 -64
  78. package/dist/components/meeting-scheduler/index.js.map +1 -1
  79. package/dist/components/navigation/index.cjs +6 -6
  80. package/dist/components/navigation/index.js +5 -5
  81. package/dist/components/onboarding-guides/index.cjs +6 -6
  82. package/dist/components/onboarding-guides/index.js +5 -5
  83. package/dist/components/tickets/index.cjs +7 -7
  84. package/dist/components/tickets/index.js +6 -6
  85. package/dist/components/ui/benefit-card.d.ts +7 -0
  86. package/dist/components/ui/benefit-card.d.ts.map +1 -1
  87. package/dist/components/ui/field-wrapper.d.ts +4 -4
  88. package/dist/components/ui/file-manager/index.cjs +4 -4
  89. package/dist/components/ui/file-manager/index.js +1 -1
  90. package/dist/components/ui/index.cjs +5 -3
  91. package/dist/components/ui/index.cjs.map +1 -1
  92. package/dist/components/ui/index.js +4 -2
  93. package/dist/index.cjs +9 -7
  94. package/dist/index.cjs.map +1 -1
  95. package/dist/index.js +8 -6
  96. package/dist/index.js.map +1 -1
  97. package/dist/schemas/meeting-booking-schema.cjs.map +1 -1
  98. package/dist/schemas/meeting-booking-schema.d.ts.map +1 -1
  99. package/dist/schemas/meeting-booking-schema.js.map +1 -1
  100. package/dist/utils/hubspot-meetings-convention.d.ts +1 -1
  101. package/dist/utils/hubspot-meetings-convention.d.ts.map +1 -1
  102. package/dist/utils/index.cjs.map +1 -1
  103. package/dist/utils/index.js.map +1 -1
  104. package/package.json +1 -1
  105. package/src/__tests__/benefit-card-border.test.ts +29 -0
  106. package/src/__tests__/booking-form-field-rows.test.tsx +9 -47
  107. package/src/__tests__/booking-form-number-field.test.tsx +14 -44
  108. package/src/__tests__/fixtures/meeting-booking.tsx +61 -0
  109. package/src/__tests__/meeting-scheduler-details-first.test.tsx +3 -6
  110. package/src/components/meeting-scheduler/booking-form.tsx +96 -70
  111. package/src/components/meeting-scheduler/index.tsx +24 -14
  112. package/src/components/ui/benefit-card.tsx +45 -39
  113. package/src/components/ui/field-wrapper.tsx +4 -4
  114. package/src/schemas/meeting-booking-schema.ts +1 -0
  115. package/src/utils/hubspot-meetings-convention.ts +1 -1
  116. package/dist/chunk-DR4WXTKP.cjs.map +0 -1
  117. package/dist/chunk-S2WP7ELW.js.map +0 -1
  118. package/src/__tests__/fixtures/meeting-booking.ts +0 -27
  119. /package/dist/{chunk-WTK5YZYU.js.map → chunk-A6YERC4N.js.map} +0 -0
  120. /package/dist/{chunk-XNZMKCA4.js.map → chunk-AGKFT4FA.js.map} +0 -0
  121. /package/dist/{chunk-QAJXIOK2.js.map → chunk-AYMBMVOD.js.map} +0 -0
  122. /package/dist/{chunk-7XHONLYQ.js.map → chunk-BLYQD3PG.js.map} +0 -0
  123. /package/dist/{chunk-E7FDTOJ6.js.map → chunk-FGHJ3BC7.js.map} +0 -0
  124. /package/dist/{chunk-E6NN7MAN.js.map → chunk-FIQIT6HX.js.map} +0 -0
  125. /package/dist/{chunk-J2OCCRFC.js.map → chunk-GZ76RASF.js.map} +0 -0
  126. /package/dist/{chunk-ESHCKTES.js.map → chunk-J5EA7PMY.js.map} +0 -0
  127. /package/dist/{chunk-3PUXHFC4.js.map → chunk-JVYNMMNE.js.map} +0 -0
  128. /package/dist/{chunk-OKCGXXEK.js.map → chunk-PO4ATWE4.js.map} +0 -0
  129. /package/dist/{chunk-X6TLDYMG.js.map → chunk-QNZM43HU.js.map} +0 -0
  130. /package/dist/{chunk-IJ4SGRAN.js.map → chunk-S7LV66HD.js.map} +0 -0
  131. /package/dist/{chunk-EM3T7YXE.js.map → chunk-Y6JB7X5Y.js.map} +0 -0
  132. /package/dist/{chunk-YZALGXOM.js.map → chunk-ZVBYWM5Z.js.map} +0 -0
@@ -55,7 +55,7 @@ import {
55
55
  import { cn } from '../../utils/cn';
56
56
  import { formatDurationCompact } from '../../utils/format';
57
57
  import { Alert, AlertDescription, Button } from '../ui';
58
- import { BookingForm, BookingFormSkeleton, type BookingFormProps } from './booking-form';
58
+ import { BookingForm, BookingFormSkeleton, DEFAULT_SUBMIT_LABEL, type BookingFormProps } from './booking-form';
59
59
  import { Confirmation } from './confirmation';
60
60
  import { SchedulerContextPanel, ContextPanelSkeleton } from './context-panel';
61
61
  import { SlotPicker, SlotPickerSkeleton, dayKeyInZone } from './slot-picker';
@@ -186,6 +186,8 @@ const ACTION_PANEL_CLASS = 'flex-1 min-w-0 flex flex-col md:min-h-0 lg:p-[var(--
186
186
  * being cut off at the card's edge.
187
187
  */
188
188
  const PANEL_STEP_CLASS = 'p-[var(--spacing-system-l)] md:min-h-0 md:overflow-y-auto lg:p-0';
189
+ /** A step's panel: the inset above on a column that fills the action side. */
190
+ const STEP_PANEL_CLASS = cn('flex flex-1 flex-col', PANEL_STEP_CLASS);
189
191
 
190
192
  /**
191
193
  * The widget's height on desktop: a FIXED 380px, and the ONLY size the whole
@@ -255,6 +257,9 @@ export const MEETING_SCHEDULER_DETAILS_FIRST_H = 'md:h-[39.875rem]';
255
257
 
256
258
  export type SchedulerFlow = 'slot-first' | 'details-first';
257
259
 
260
+ /** The flow every existing embed gets — the ONE spelling of the default. */
261
+ export const DEFAULT_SCHEDULER_FLOW: SchedulerFlow = 'slot-first';
262
+
258
263
  /**
259
264
  * What differs between the two flows as DATA — first step, the box a host
260
265
  * reserves, the form's submit copy. Exported so a host that swaps the card in
@@ -266,7 +271,7 @@ export const SCHEDULER_FLOW_PRESETS: Record<
266
271
  SchedulerFlow,
267
272
  { initialStep: Exclude<Step, 'confirmed'>; height: string; submitLabel: string; footerNote?: string }
268
273
  > = {
269
- 'slot-first': { initialStep: 'slot', height: MEETING_SCHEDULER_H, submitLabel: 'Confirm Booking' },
274
+ 'slot-first': { initialStep: 'slot', height: MEETING_SCHEDULER_H, submitLabel: DEFAULT_SUBMIT_LABEL },
270
275
  'details-first': {
271
276
  initialStep: 'details',
272
277
  height: MEETING_SCHEDULER_DETAILS_FIRST_H,
@@ -334,7 +339,7 @@ const CARD_DEGRADED_CLASS = cn(
334
339
  * from {@link SCHEDULER_FLOW_PRESETS}, so the pairing is never re-derived.
335
340
  */
336
341
  export function SchedulerDegradedCard({
337
- flow = 'slot-first',
342
+ flow = DEFAULT_SCHEDULER_FLOW,
338
343
  className,
339
344
  message,
340
345
  action,
@@ -385,7 +390,7 @@ export function HubSpotMeetingScheduler({
385
390
  onBack,
386
391
  onBooked,
387
392
  className,
388
- flow = 'slot-first',
393
+ flow = DEFAULT_SCHEDULER_FLOW,
389
394
  detailsForm: DetailsForm = BookingForm,
390
395
  detailsFormProps,
391
396
  }: HubSpotMeetingSchedulerProps) {
@@ -665,6 +670,11 @@ export function HubSpotMeetingScheduler({
665
670
  * last-wins, so appending it after would make a host's own `h-*` unreachable,
666
671
  * which is the override the height-inside-CARD_CLASS arrangement allows today.
667
672
  */
673
+ /** The card's ONE back edge, per flow and step — the skeleton and the loaded
674
+ * panel wire the same one, so a month load can never swap it for the host's
675
+ * exit under a details-first visitor. */
676
+ const backEdge = detailsFirst ? (step === 'slot' ? backToDetails : onBack) : step === 'details' ? backToSlot : onBack;
677
+
668
678
  const cardClass = cn(CARD_CLASS, preset.height, formOnly && 'bg-ods-bg', className);
669
679
 
670
680
  /** One card SHAPE for every degraded return below. */
@@ -681,7 +691,7 @@ export function HubSpotMeetingScheduler({
681
691
  return (
682
692
  <div className={cardClass}>
683
693
  <div className={CARD_INNER_CLASS}>
684
- {!formOnly && <ContextPanelSkeleton onBack={onBack} className={CONTEXT_PANEL_CLASS} />}
694
+ {!formOnly && <ContextPanelSkeleton onBack={backEdge} className={CONTEXT_PANEL_CLASS} />}
685
695
  <div className={ACTION_PANEL_CLASS}>
686
696
  {/* Both terms matter. Without `flow` this stays a calendar where
687
697
  the form belongs; without the step term, paging a month AFTER
@@ -690,8 +700,8 @@ export function HubSpotMeetingScheduler({
690
700
  {detailsFirst && step === 'details' ? (
691
701
  // The SAME wrapper the loaded form gets, so the skeleton never
692
702
  // states its own inset.
693
- <div className={cn('flex flex-1 flex-col', PANEL_STEP_CLASS)}>
694
- <BookingFormSkeleton fieldRows={detailsFormProps?.fieldRows} />
703
+ <div className={STEP_PANEL_CLASS}>
704
+ <BookingFormSkeleton fieldRows={detailsFormProps?.fieldRows} footerNote={preset.footerNote} />
695
705
  </div>
696
706
  ) : (
697
707
  <SlotPickerSkeleton monthOffset={monthOffset} />
@@ -742,9 +752,7 @@ export function HubSpotMeetingScheduler({
742
752
  // details-first inverts the mapping: Back at `slot` returns to the
743
753
  // form. At `details` the form-only layout mounts no panel, so a host
744
754
  // exit is not rendered there (the page around the card is the exit).
745
- onBack={
746
- detailsFirst ? (step === 'slot' ? backToDetails : onBack) : step === 'details' ? backToSlot : onBack
747
- }
755
+ onBack={backEdge}
748
756
  // `locked` HIDES the selectors (post-selection steps); `disabled`
749
757
  // keeps them mounted but inert. details-first needs the second while
750
758
  // a POST is in flight: a mid-flight change would clear the slot
@@ -765,17 +773,19 @@ export function HubSpotMeetingScheduler({
765
773
  default `min-height:auto` refuses to shrink below its content. */}
766
774
  <div className={ACTION_PANEL_CLASS}>
767
775
  {step === 'confirmed' && confirmation && timezone ? (
768
- <div className={cn('flex flex-1 flex-col', PANEL_STEP_CLASS)}>
776
+ <div className={STEP_PANEL_CLASS}>
769
777
  <Confirmation confirmation={confirmation} timezone={timezone} />
770
778
  </div>
771
779
  ) : step === 'details' &&
772
780
  (detailsFirst
773
781
  ? // The form is step ONE here: no slot yet, and `timezone` is null
774
782
  // on the server render, so neither may gate the PANEL. A link
775
- // publishing no durations has no form worth showing.
776
- durations.length > 0
783
+ // publishing no durations has no form worth showing — but while a
784
+ // refetch is in flight the form stays (a calendar skeleton is
785
+ // the wrong shape here).
786
+ durations.length > 0 || isFetchingAvailability
777
787
  : durationMs != null && selectedSlot != null && timezone) ? (
778
- <div className={cn('flex flex-1 flex-col gap-[var(--spacing-system-m)]', PANEL_STEP_CLASS)}>
788
+ <div className={cn(STEP_PANEL_CLASS, 'gap-[var(--spacing-system-m)]')}>
779
789
  {/* Top-aligned, and no back edge of its own: the ONE back edge
780
790
  lives in the context panel at every step (the `onBack` wired on `SchedulerContextPanel`),
781
791
  which is where the design puts it and the only spot that
@@ -61,6 +61,50 @@ interface BenefitCardGridProps {
61
61
  columns?: 2 | 3 | 4; // Support 2, 3, or 4 columns
62
62
  }
63
63
 
64
+ /**
65
+ * Which dividers card `index` of `totalItems` draws in a `columns` grid. Pure:
66
+ * the per-breakpoint row math (a 3-column grid lays out 1-up / 2-up / 3-up, so a
67
+ * card's row differs per breakpoint) is what needs testing, and the grid itself
68
+ * is a `cloneElement` over children.
69
+ */
70
+ export function benefitCardBorderClass(index: number, totalItems: number, columns: number): string {
71
+ const isLastItem = index === totalItems - 1;
72
+ if (columns === 4) {
73
+ // 4-column grid borders
74
+ const isLastInRow = (index + 1) % 4 === 0;
75
+ const isInLastRow = index >= totalItems - 4;
76
+ return cn(
77
+ !isLastInRow && 'border-ods-border md:border-r',
78
+ !isInLastRow && 'border-b md:border-b lg:border-b-0',
79
+ index < 2 && 'lg:border-b-0',
80
+ );
81
+ } else if (columns === 3) {
82
+ // 3 columns lay out 1-up / 2-up / 3-up, so the row a card sits in
83
+ // differs per breakpoint and a single rule cannot describe it. The
84
+ // old shared 2-3 branch assumed one row and, at `md` (grid-cols-2),
85
+ // gave card 1 a trailing `md:border-r` while suppressing the divider
86
+ // between the two rows.
87
+ const isLastInMdRow = (index + 1) % 2 === 0;
88
+ const isInLastMdRow = index >= totalItems - (totalItems % 2 === 0 ? 2 : 1);
89
+ const isLastInLgRow = (index + 1) % 3 === 0;
90
+ const isInLastLgRow = index >= totalItems - (totalItems % 3 === 0 ? 3 : totalItems % 3);
91
+ return cn(
92
+ 'border-ods-border',
93
+ !isLastItem && 'border-b',
94
+ isInLastMdRow && 'md:border-b-0',
95
+ !isLastInMdRow && !isLastItem && 'md:border-r',
96
+ isLastInMdRow && 'md:border-r-0',
97
+ isInLastLgRow && 'lg:border-b-0',
98
+ !isInLastMdRow && !isInLastLgRow && 'lg:border-b',
99
+ !isLastInLgRow && !isLastItem && 'lg:border-r',
100
+ isLastInLgRow && 'lg:border-r-0',
101
+ );
102
+ } else {
103
+ // 2 columns: a flex row, so one rule genuinely does describe it.
104
+ return isLastItem ? 'border-b-0' : 'border-b md:border-b-0 md:border-r border-ods-border';
105
+ }
106
+ }
107
+
64
108
  export const BenefitCardGrid: React.FC<BenefitCardGridProps> = ({ children, className = '', columns = 2 }) => {
65
109
  const childrenArray = React.Children.toArray(children);
66
110
 
@@ -80,45 +124,7 @@ export const BenefitCardGrid: React.FC<BenefitCardGridProps> = ({ children, clas
80
124
  )}
81
125
  >
82
126
  {childrenArray.map((child, index) => {
83
- const totalItems = childrenArray.length;
84
- const isLastItem = index === totalItems - 1;
85
-
86
- // Dynamic border logic based on columns
87
- let borderClass = '';
88
- if (columns === 4) {
89
- // 4-column grid borders
90
- const isLastInRow = (index + 1) % 4 === 0;
91
- const isInLastRow = index >= totalItems - 4;
92
- borderClass = cn(
93
- !isLastInRow && 'border-ods-border md:border-r',
94
- !isInLastRow && 'border-b md:border-b lg:border-b-0',
95
- index < 2 && 'lg:border-b-0',
96
- );
97
- } else if (columns === 3) {
98
- // 3 columns lay out 1-up / 2-up / 3-up, so the row a card sits in
99
- // differs per breakpoint and a single rule cannot describe it. The
100
- // old shared 2-3 branch assumed one row and, at `md` (grid-cols-2),
101
- // gave card 1 a trailing `md:border-r` while suppressing the divider
102
- // between the two rows.
103
- const isLastInMdRow = (index + 1) % 2 === 0;
104
- const isInLastMdRow = index >= totalItems - (totalItems % 2 === 0 ? 2 : 1);
105
- const isLastInLgRow = (index + 1) % 3 === 0;
106
- const isInLastLgRow = index >= totalItems - (totalItems % 3 === 0 ? 3 : totalItems % 3);
107
- borderClass = cn(
108
- 'border-ods-border',
109
- !isLastItem && 'border-b',
110
- isInLastMdRow && 'md:border-b-0',
111
- !isLastInMdRow && !isLastItem && 'md:border-r',
112
- isLastInMdRow && 'md:border-r-0',
113
- isInLastLgRow && 'lg:border-b-0',
114
- !isInLastMdRow && !isInLastLgRow && 'lg:border-b',
115
- !isLastInLgRow && !isLastItem && 'lg:border-r',
116
- isLastInLgRow && 'lg:border-r-0',
117
- );
118
- } else {
119
- // 2 columns: a flex row, so one rule genuinely does describe it.
120
- borderClass = isLastItem ? 'border-b-0' : 'border-b md:border-b-0 md:border-r border-ods-border';
121
- }
127
+ const borderClass = benefitCardBorderClass(index, childrenArray.length, columns);
122
128
 
123
129
  return React.cloneElement(child as React.ReactElement<{ className?: string }>, {
124
130
  key: index,
@@ -36,10 +36,10 @@ export interface FieldWrapperProps {
36
36
  */
37
37
  labelVariant?: 'default' | 'large';
38
38
  /**
39
- * Marks the label with the `RequiredMark` `ContactForm` draws on its
40
- * required fields, so a form
41
- * mixing both never shows two conventions. Visual only — the CONTROL carries
42
- * `required`/`aria-required` for assistive tech; the star is `aria-hidden`.
39
+ * Marks the label with the `RequiredMark` `ContactForm` draws on its required
40
+ * fields, so a form mixing both never shows two conventions. Visual only — the
41
+ * CONTROL carries `required`/`aria-required` for assistive tech; the star is
42
+ * `aria-hidden`.
43
43
  */
44
44
  required?: boolean;
45
45
  /** Additional className for the outer wrapper */
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+
2
3
  /**
3
4
  * Meeting-booking wire contracts + validation factory.
4
5
  *
@@ -32,12 +32,12 @@ export const SCHEDULING_SLUG_SHAPE = /^[a-z0-9-]+(\/[a-z0-9-]+)*$/;
32
32
  /** Upper bound for availability month paging (widget nav, route 400, DAL clamp). */
33
33
  export const MAX_MONTH_OFFSET = 11;
34
34
 
35
- /** Full-slug shape validity (path-resolution safety only — never semantics). */
36
35
  /** The one spelling of slug normalisation — every map key and lookup goes through it. */
37
36
  export function normalizeSchedulingSlug(slug: string): string {
38
37
  return slug.trim().toLowerCase();
39
38
  }
40
39
 
40
+ /** Full-slug shape validity (path-resolution safety only — never semantics). */
41
41
  export function isValidSchedulingSlug(slug: string): boolean {
42
42
  return SCHEDULING_SLUG_SHAPE.test(normalizeSchedulingSlug(slug));
43
43
  }