riverbankcms 0.5.0 → 0.7.0

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.
@@ -52,7 +52,7 @@ const unsafeAsOccurrenceId = (id) => id;
52
52
  const unsafeAsEventOfferingId = (id) => id;
53
53
  const unsafeAsEventSeriesId = (id) => id;
54
54
  const unsafeAsEventPageRouteSlug = (value) => value;
55
- const unsafeAsTicketTypeId = (id) => id;
55
+ const unsafeAsEventAttendeeTicketLineId = (id) => id;
56
56
  const unsafeAsEventSeriesRegistrationId = (id) => id;
57
57
  const unsafeAsEventCategoryId = (id) => id;
58
58
  const unsafeAsVenueId = (id) => id;
@@ -90,8 +90,6 @@ const unsafeAsFormId = (id) => id;
90
90
  const unsafeAsFormSubmissionId = (id) => id;
91
91
  const unsafeAsPassId = (id) => id;
92
92
  const unsafeAsMembershipId = (id) => id;
93
- const unsafeAsCourseId = (id) => id;
94
- const unsafeAsCourseProgramId = (id) => id;
95
93
  const unsafeAsCustomerId = (id) => id;
96
94
  const unsafeAsOrderId = (id) => id;
97
95
  const unsafeAsOrderItemId = (id) => id;
@@ -103,6 +101,9 @@ const unsafeAsOrderPaymentPlanInstalmentId = (id) => id;
103
101
  const unsafeAsManualPaymentOperationId = (id) => id;
104
102
  const unsafeAsCheckoutLineId = (id) => id;
105
103
  const unsafeAsAttendeeId = (id) => id;
104
+ const unsafeAsEventRegistrationCommandId = (id) => id;
105
+ const unsafeAsEventRegistrationCommandRowId = (id) => id;
106
+ const unsafeAsEventRegistrationProjectionClaimToken = (id) => id;
106
107
  const unsafeAsCustomerPassId = (id) => id;
107
108
  const unsafeAsCustomerMembershipId = (id) => id;
108
109
  const unsafeAsAppointmentId = (id) => id;
@@ -160,8 +161,7 @@ const parsePageId = (value) => parseUuidBrand(value, unsafeAsPageId);
160
161
  const parsePageSurfaceId = (value) => parseUuidBrand(value, unsafeAsPageSurfaceId);
161
162
  const parseOccurrenceId = (value) => parseUuidBrand(value, unsafeAsOccurrenceId);
162
163
  const parseEventOfferingId = (value) => parseUuidBrand(value, unsafeAsEventOfferingId);
163
- const parseCourseProgramId = (value) => parseUuidBrand(value, unsafeAsCourseProgramId);
164
- const parseTicketTypeId = (value) => parseUuidBrand(value, unsafeAsTicketTypeId);
164
+ const parseEventAttendeeTicketLineId = (value) => parseUuidBrand(value, unsafeAsEventAttendeeTicketLineId);
165
165
  const parsePricingOptionId = (value) => parseUuidBrand(value, unsafeAsPricingOptionId);
166
166
  const parseUserId = (value) => parseUuidBrand(value, unsafeAsUserId);
167
167
  const parseProductId = (value) => parseUuidBrand(value, unsafeAsProductId);
@@ -175,7 +175,8 @@ const parseNavigationMenuId = (value) => parseUuidBrand(value, unsafeAsNavigatio
175
175
  const parseNavigationItemId = (value) => parseUuidBrand(value, unsafeAsNavigationItemId);
176
176
  const parsePassId = (value) => parseUuidBrand(value, unsafeAsPassId);
177
177
  const parseMembershipId = (value) => parseUuidBrand(value, unsafeAsMembershipId);
178
- const parseCourseId = (value) => parseUuidBrand(value, unsafeAsCourseId);
178
+ const asRetiredCourseId = (id) => id;
179
+ const parseRetiredCourseId = (value) => parseUuidBrand(value, asRetiredCourseId);
179
180
  const parseFormId = (value) => parseUuidBrand(value, unsafeAsFormId);
180
181
  const parseAppointmentId = (value) => parseUuidBrand(value, unsafeAsAppointmentId);
181
182
  const parseAppointmentServiceId = (value) => parseUuidBrand(value, unsafeAsAppointmentServiceId);
@@ -183,6 +184,9 @@ const parseAppointmentResourceId = (value) => parseUuidBrand(value, unsafeAsAppo
183
184
  const parseAppointmentReservationId = (value) => parseUuidBrand(value, unsafeAsAppointmentReservationId);
184
185
  const parseAppointmentReservationRunId = (value) => parseUuidBrand(value, unsafeAsAppointmentReservationRunId);
185
186
  const parseAttendeeId = (value) => parseUuidBrand(value, unsafeAsAttendeeId);
187
+ const parseEventRegistrationCommandId = (value) => parseUuidBrand(value, unsafeAsEventRegistrationCommandId);
188
+ const parseEventRegistrationCommandRowId = (value) => parseUuidBrand(value, unsafeAsEventRegistrationCommandRowId);
189
+ const parseEventRegistrationProjectionClaimToken = (value) => parseUuidBrand(value, unsafeAsEventRegistrationProjectionClaimToken);
186
190
  const parseDatedOfferingCapacityHoldId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingCapacityHoldId);
187
191
  const parseDatedOfferingCapacityHoldAuditRef = (value) => parseUuidBrand(value, unsafeAsDatedOfferingCapacityHoldAuditRef);
188
192
  const parseDatedOfferingRevocationAuditEventId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingRevocationAuditEventId);
@@ -475,6 +479,44 @@ function assertNever(value, message = "Unexpected variant") {
475
479
  throw new Error(`${message}: ${String(value)}`);
476
480
  }
477
481
 
482
+ //#endregion
483
+ //#region ../core/src/event-booking-offer.ts
484
+ const EVENT_PRICING_STRATEGIES = [
485
+ "free",
486
+ "single",
487
+ "tiered",
488
+ "sliding_scale"
489
+ ];
490
+ function createEventBookingOffer(input) {
491
+ if (input.bookingRequirement === "none") {
492
+ return input.pricing.kind === "free" ? ok({
493
+ bookingRequirement: "none",
494
+ pricing: input.pricing,
495
+ paymentTerms: input.paymentTerms
496
+ }) : err({ kind: "paid_without_registration" });
497
+ }
498
+ if (input.bookingRequirement === "required") {
499
+ return ok({
500
+ bookingRequirement: "required",
501
+ pricing: input.pricing,
502
+ paymentTerms: input.paymentTerms
503
+ });
504
+ }
505
+ return err({
506
+ kind: "invalid_booking_requirement",
507
+ value: input.bookingRequirement
508
+ });
509
+ }
510
+ function eventPricingRequiresPayment(pricing) {
511
+ switch (pricing.kind) {
512
+ case "free": return false;
513
+ case "single": return true;
514
+ case "tiered": return pricing.options.some((option) => option.priceAmount > 0);
515
+ case "sliding_scale": return pricing.maximum > 0;
516
+ default: return assertNever(pricing);
517
+ }
518
+ }
519
+
478
520
  //#endregion
479
521
  //#region ../core/src/currency-format.ts
480
522
  const CURRENCY_CODE_INPUT_PATTERN = /^[A-Za-z]{3}$/;
@@ -538,183 +580,6 @@ function formatCurrencyAmount(amountCents, currency, options) {
538
580
  }
539
581
  }
540
582
 
541
- //#endregion
542
- //#region ../core/src/event-pricing.ts
543
- const EVENT_PRICING_MODES = [
544
- "single",
545
- "ticket_types",
546
- "sliding_scale"
547
- ];
548
- function isEventPricingMode(value) {
549
- return typeof value === "string" && EVENT_PRICING_MODES.includes(value);
550
- }
551
- function publicEventPricingDisplay(pricing) {
552
- return pricing.display;
553
- }
554
- function publicEventPricingType(pricing) {
555
- switch (pricing.kind) {
556
- case "drop_in":
557
- case "free_registration": return "free";
558
- case "single_paid":
559
- case "ticket_types":
560
- case "sliding_scale":
561
- case "whole_series": return isFreePublicEventPriceDisplay(pricing.display) ? "free" : "paid";
562
- case "external": return "external";
563
- }
564
- }
565
- function publicEventPricingCurrency(pricing) {
566
- switch (pricing.kind) {
567
- case "drop_in":
568
- case "free_registration":
569
- case "single_paid":
570
- case "ticket_types":
571
- case "sliding_scale": return pricing.currency;
572
- case "whole_series": return pricing.display.display.currency;
573
- case "external": return null;
574
- }
575
- }
576
- function publicEventScalarPrice(pricing) {
577
- return pricing.kind === "single_paid" ? pricing.amount : null;
578
- }
579
- function publicEventPricingMode(pricing) {
580
- switch (pricing.kind) {
581
- case "single_paid":
582
- case "free_registration": return "single";
583
- case "ticket_types": return "ticket_types";
584
- case "sliding_scale": return "sliding_scale";
585
- case "drop_in":
586
- case "whole_series":
587
- case "external": return undefined;
588
- }
589
- }
590
- function publicEventTicketTypes(pricing) {
591
- return pricing.kind === "ticket_types" ? pricing.ticketTypes : [];
592
- }
593
- function isFreePublicEventPriceDisplay(display) {
594
- switch (display.kind) {
595
- case "free": return true;
596
- case "whole_series": return display.display.kind === "free";
597
- case "exact":
598
- case "from":
599
- case "range":
600
- case "mixed":
601
- case "unavailable":
602
- case "external": return false;
603
- }
604
- }
605
- function formatCoursePriceDisplayLabel(display, options) {
606
- switch (display.kind) {
607
- case "free":
608
- case "unavailable": return "Course";
609
- case "exact": return `Course (${formatCurrencyAmount(display.amount, display.currency, options)})`;
610
- case "from": return `Course (From ${formatCurrencyAmount(display.amount, display.currency, options)})`;
611
- case "range": return `Course (${formatCurrencyAmount(display.minAmount, display.currency, options)} - ${formatCurrencyAmount(display.maxAmount, display.currency, options)})`;
612
- case "mixed": return display.paidToAmount > display.paidFromAmount ? `Course (Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)} - ${formatCurrencyAmount(display.paidToAmount, display.currency, options)})` : `Course (Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)})`;
613
- default: return assertNever(display);
614
- }
615
- }
616
- /**
617
- * The single renderer for a precomputed booking price `display` ADT. This is
618
- * the one place booking price variants map to user-facing strings — every call
619
- * site (booking entry, events calendar, event/course details, listings, portal)
620
- * should route through here so labels never drift. Adding a variant to the ADT
621
- * breaks compilation here (via `assertNever`) until it is handled.
622
- */
623
- function formatBookingPriceDisplay(display, options = {}) {
624
- switch (display.kind) {
625
- case "free": return "Free";
626
- case "exact": return formatCurrencyAmount(display.amount, display.currency, options);
627
- case "from": return `From ${formatCurrencyAmount(display.amount, display.currency, options)}`;
628
- case "range": return `${formatCurrencyAmount(display.minAmount, display.currency, options)} - ${formatCurrencyAmount(display.maxAmount, display.currency, options)}`;
629
- case "mixed": return display.paidToAmount > display.paidFromAmount ? `Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)} - ${formatCurrencyAmount(display.paidToAmount, display.currency, options)}` : `Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)}`;
630
- case "unavailable": return "Pricing unavailable";
631
- case "external": return display.label;
632
- case "whole_series": return formatCoursePriceDisplayLabel(display.display, options);
633
- default: return assertNever(display);
634
- }
635
- }
636
- /** Render an event's price directly from its pricing value. */
637
- function formatEventPricing(pricing, options = {}) {
638
- return formatBookingPriceDisplay(publicEventPricingDisplay(pricing), options);
639
- }
640
-
641
- //#endregion
642
- //#region ../core/src/event-page-surface-subject.ts
643
- function createEventSeriesPageSurfaceSubject(eventSeriesId, bookingScope) {
644
- return bookingScope === "whole_series" ? {
645
- kind: "eventSeries",
646
- eventSeriesId,
647
- bookingScope,
648
- isWholeSeriesBooking: true
649
- } : {
650
- kind: "eventSeries",
651
- eventSeriesId,
652
- bookingScope
653
- };
654
- }
655
- function parseEventPageSurfaceSubject(value) {
656
- if (!isRecord$6(value)) return err({ kind: "notObject" });
657
- switch (value.kind) {
658
- case "eventSeries": {
659
- const eventSeriesId = parseEventSeriesId(value.eventSeriesId);
660
- if (!eventSeriesId.ok) {
661
- return err({
662
- kind: "invalidEventSeriesId",
663
- error: eventSeriesId.error
664
- });
665
- }
666
- if (value.bookingScope !== "single_occurrence" && value.bookingScope !== "whole_series") {
667
- return err({
668
- kind: "invalidEventSeriesBookingScope",
669
- bookingScope: value.bookingScope
670
- });
671
- }
672
- return ok(createEventSeriesPageSurfaceSubject(eventSeriesId.value, value.bookingScope));
673
- }
674
- case "eventOffering": {
675
- const eventOfferingId = parseEventOfferingId(value.eventOfferingId);
676
- if (!eventOfferingId.ok) {
677
- return err({
678
- kind: "invalidEventOfferingId",
679
- error: eventOfferingId.error
680
- });
681
- }
682
- if (!Array.isArray(value.availableEventSeriesIds)) {
683
- return err({
684
- kind: "unsupportedSubject",
685
- subjectKind: value.kind
686
- });
687
- }
688
- const parsedSeriesIds = [];
689
- for (const valueId of value.availableEventSeriesIds) {
690
- const eventSeriesId = parseEventSeriesId(valueId);
691
- if (!eventSeriesId.ok) {
692
- return err({
693
- kind: "invalidEventSeriesId",
694
- error: eventSeriesId.error
695
- });
696
- }
697
- parsedSeriesIds.push(eventSeriesId.value);
698
- }
699
- return ok({
700
- kind: "eventOffering",
701
- eventOfferingId: eventOfferingId.value,
702
- availableEventSeriesIds: parsedSeriesIds
703
- });
704
- }
705
- default: return err({
706
- kind: "unsupportedSubject",
707
- subjectKind: value.kind
708
- });
709
- }
710
- }
711
- function toEventPageSurfaceSubjectWire(subject) {
712
- return subject;
713
- }
714
- function isRecord$6(value) {
715
- return typeof value === "object" && value !== null && !Array.isArray(value);
716
- }
717
-
718
583
  //#endregion
719
584
  //#region ../core/src/money-input.ts
720
585
  function unsafeAsCents(value) {
@@ -843,6 +708,675 @@ function formatMoneyInputFromCents(amountCents) {
843
708
  return `${sign}${units}.${fractional}`;
844
709
  }
845
710
 
711
+ //#endregion
712
+ //#region ../core/src/non-empty-array.ts
713
+ function fromArray(items) {
714
+ const [first, ...rest] = items;
715
+ if (first === undefined) {
716
+ return err("empty_array");
717
+ }
718
+ return ok([first, ...rest]);
719
+ }
720
+ function mapNonEmptyArray(items, map) {
721
+ return items.map(map);
722
+ }
723
+ function fromReadonlyArray(items) {
724
+ const [first, ...rest] = items;
725
+ return first === undefined ? err("empty_array") : ok([first, ...rest]);
726
+ }
727
+ function mapReadonlyNonEmptyArray(items, map) {
728
+ const [first, ...rest] = items;
729
+ return [map(first, 0), ...rest.map((item, index) => map(item, index + 1))];
730
+ }
731
+
732
+ //#endregion
733
+ //#region ../core/src/event-booking-offer-parser.ts
734
+ function oneIssue(issue) {
735
+ return [issue];
736
+ }
737
+ function isNullish(value) {
738
+ return value === null || value === undefined;
739
+ }
740
+ function parseOptionIdentity(input) {
741
+ const id = parsePricingOptionId(input.id);
742
+ if (!id.ok) return err({ kind: "invalid_id" });
743
+ if (typeof input.name !== "string" || input.name.trim().length === 0) {
744
+ return err({ kind: "invalid_name" });
745
+ }
746
+ if (!isNullish(input.description) && typeof input.description !== "string") {
747
+ return err({ kind: "invalid_description" });
748
+ }
749
+ return ok({
750
+ id: id.value,
751
+ name: input.name.trim(),
752
+ description: input.description ?? null
753
+ });
754
+ }
755
+ function parseOptionCommercials(input) {
756
+ const price = typeof input.priceAmount === "number" ? parseNonNegativeCents(input.priceAmount) : err({ kind: "invalid-number" });
757
+ if (!price.ok) return err({
758
+ kind: "invalid_price",
759
+ error: price.error
760
+ });
761
+ if (typeof input.isActive !== "boolean") return err({ kind: "invalid_active" });
762
+ if (typeof input.sortOrder !== "number" || !Number.isInteger(input.sortOrder) || input.sortOrder < 0) {
763
+ return err({ kind: "invalid_sort_order" });
764
+ }
765
+ if (!isNullish(input.capacityLimit) && (typeof input.capacityLimit !== "number" || !Number.isInteger(input.capacityLimit) || input.capacityLimit < 1)) {
766
+ return err({ kind: "invalid_capacity" });
767
+ }
768
+ return ok({
769
+ priceAmount: price.value,
770
+ isActive: input.isActive,
771
+ sortOrder: input.sortOrder,
772
+ capacityLimit: input.capacityLimit ?? null
773
+ });
774
+ }
775
+ function parseNullableIsoDateTime(value, issue) {
776
+ if (isNullish(value)) return ok(null);
777
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
778
+ return err({ kind: issue });
779
+ }
780
+ return ok(value);
781
+ }
782
+ function parseAttendanceScope(value) {
783
+ if (value === "in_person" || value === "online" || value === "both") {
784
+ return ok(value);
785
+ }
786
+ return err({ kind: "invalid_attendance_scope" });
787
+ }
788
+ function parseRecipeIdentifier(value) {
789
+ if (isNullish(value)) return ok(null);
790
+ if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(value)) {
791
+ return err({ kind: "invalid_recipe_identifier" });
792
+ }
793
+ return ok(value);
794
+ }
795
+ function parseOptionAvailability(input) {
796
+ const start = parseNullableIsoDateTime(input.saleStartsAt, "invalid_sale_start");
797
+ if (!start.ok) return start;
798
+ const end = parseNullableIsoDateTime(input.saleEndsAt, "invalid_sale_end");
799
+ if (!end.ok) return end;
800
+ if (start.value !== null && end.value !== null && Date.parse(end.value) <= Date.parse(start.value)) {
801
+ return err({ kind: "invalid_sale_window" });
802
+ }
803
+ const scope = parseAttendanceScope(input.attendanceScope);
804
+ if (!scope.ok) return scope;
805
+ const recipe = parseRecipeIdentifier(input.recipeIdentifier);
806
+ if (!recipe.ok) return recipe;
807
+ return ok({
808
+ saleStartsAt: start.value,
809
+ saleEndsAt: end.value,
810
+ attendanceScope: scope.value,
811
+ recipeIdentifier: recipe.value
812
+ });
813
+ }
814
+ function parseEventPricingOption(input) {
815
+ const identity = parseOptionIdentity(input);
816
+ const commercials = parseOptionCommercials(input);
817
+ const availability$1 = parseOptionAvailability(input);
818
+ const issues = [
819
+ identity,
820
+ commercials,
821
+ availability$1
822
+ ].flatMap((result) => result.ok ? [] : [result.error]);
823
+ if (!identity.ok || !commercials.ok || !availability$1.ok) {
824
+ const nonEmptyIssues = fromReadonlyArray(issues);
825
+ return err(nonEmptyIssues.ok ? nonEmptyIssues.value : oneIssue({ kind: "invalid_object" }));
826
+ }
827
+ return ok({
828
+ ...identity.value,
829
+ ...commercials.value,
830
+ ...availability$1.value
831
+ });
832
+ }
833
+ function unexpectedValues(input, fields) {
834
+ return fields.flatMap((field) => isNullish(input[field]) ? [] : [{
835
+ kind: "unexpected_value",
836
+ field
837
+ }]);
838
+ }
839
+ function invalidOptionsIssue(value) {
840
+ return Array.isArray(value) && value.length === 0 ? [] : [{
841
+ kind: "unexpected_value",
842
+ field: "options"
843
+ }];
844
+ }
845
+ function parseFreePricing(input, currency) {
846
+ const issues = [...unexpectedValues(input, [
847
+ "singlePriceAmount",
848
+ "slidingMinAmount",
849
+ "slidingMaxAmount",
850
+ "slidingSuggestedAmount"
851
+ ]), ...invalidOptionsIssue(input.options)];
852
+ const nonEmptyIssues = fromReadonlyArray(issues);
853
+ return nonEmptyIssues.ok ? err(nonEmptyIssues.value) : ok({
854
+ kind: "free",
855
+ currency
856
+ });
857
+ }
858
+ function parseSinglePricing(input, currency) {
859
+ const amount = typeof input.singlePriceAmount === "number" ? parsePositiveCents(input.singlePriceAmount) : err({ kind: "invalid-number" });
860
+ const issues = [
861
+ ...unexpectedValues(input, [
862
+ "slidingMinAmount",
863
+ "slidingMaxAmount",
864
+ "slidingSuggestedAmount"
865
+ ]),
866
+ ...invalidOptionsIssue(input.options),
867
+ ...amount.ok ? [] : [{
868
+ kind: "invalid_amount",
869
+ field: "singlePriceAmount",
870
+ error: amount.error
871
+ }]
872
+ ];
873
+ const nonEmptyIssues = fromReadonlyArray(issues);
874
+ if (nonEmptyIssues.ok) return err(nonEmptyIssues.value);
875
+ if (!amount.ok) return err(oneIssue({
876
+ kind: "invalid_amount",
877
+ field: "singlePriceAmount",
878
+ error: amount.error
879
+ }));
880
+ return ok({
881
+ kind: "single",
882
+ currency,
883
+ amount: amount.value
884
+ });
885
+ }
886
+ function parseRawPricingOption(raw, index) {
887
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
888
+ return err(oneIssue({
889
+ kind: "invalid_option",
890
+ index,
891
+ issue: { kind: "invalid_object" }
892
+ }));
893
+ }
894
+ const parsed = parseEventPricingOption(raw);
895
+ if (parsed.ok) return parsed;
896
+ const [first, ...rest] = parsed.error;
897
+ return err([{
898
+ kind: "invalid_option",
899
+ index,
900
+ issue: first
901
+ }, ...rest.map((issue) => ({
902
+ kind: "invalid_option",
903
+ index,
904
+ issue
905
+ }))]);
906
+ }
907
+ function parseTieredPricing(input, currency) {
908
+ const structuralIssues = unexpectedValues(input, [
909
+ "singlePriceAmount",
910
+ "slidingMinAmount",
911
+ "slidingMaxAmount",
912
+ "slidingSuggestedAmount"
913
+ ]);
914
+ if (!Array.isArray(input.options) || input.options.length === 0) {
915
+ return err([{ kind: "tiered_options_empty" }, ...structuralIssues]);
916
+ }
917
+ const parsed = input.options.map(parseRawPricingOption);
918
+ const issues = [...structuralIssues, ...parsed.flatMap((result) => result.ok ? [] : result.error)];
919
+ const nonEmptyIssues = fromReadonlyArray(issues);
920
+ if (nonEmptyIssues.ok) return err(nonEmptyIssues.value);
921
+ const options = parsed.flatMap((result) => result.ok ? [result.value] : []);
922
+ const nonEmptyOptions = fromReadonlyArray(options);
923
+ if (!nonEmptyOptions.ok) {
924
+ return err(oneIssue({ kind: "tiered_options_empty" }));
925
+ }
926
+ return ok({
927
+ kind: "tiered",
928
+ currency,
929
+ options: nonEmptyOptions.value
930
+ });
931
+ }
932
+ function amountIssue(field, result) {
933
+ return result.ok ? [] : [{
934
+ kind: "invalid_amount",
935
+ field,
936
+ error: result.error
937
+ }];
938
+ }
939
+ function slidingRangeIssues(minimum, maximum, suggested) {
940
+ if (!minimum.ok || !maximum.ok || !suggested.ok) return [];
941
+ const issues = [];
942
+ if (minimum.value > maximum.value) issues.push({ kind: "invalid_sliding_range" });
943
+ if (suggested.value !== null && (suggested.value < minimum.value || suggested.value > maximum.value)) {
944
+ issues.push({ kind: "suggested_outside_range" });
945
+ }
946
+ return issues;
947
+ }
948
+ function parseSlidingPricing(input, currency) {
949
+ const minimum = typeof input.slidingMinAmount === "number" ? parseNonNegativeCents(input.slidingMinAmount) : err({ kind: "invalid-number" });
950
+ const maximum = typeof input.slidingMaxAmount === "number" ? parsePositiveCents(input.slidingMaxAmount) : err({ kind: "invalid-number" });
951
+ const suggested = isNullish(input.slidingSuggestedAmount) ? ok(null) : typeof input.slidingSuggestedAmount === "number" ? parseNonNegativeCents(input.slidingSuggestedAmount) : err({ kind: "invalid-number" });
952
+ const issues = [
953
+ ...unexpectedValues(input, ["singlePriceAmount"]),
954
+ ...invalidOptionsIssue(input.options),
955
+ ...amountIssue("slidingMinAmount", minimum),
956
+ ...amountIssue("slidingMaxAmount", maximum),
957
+ ...amountIssue("slidingSuggestedAmount", suggested),
958
+ ...slidingRangeIssues(minimum, maximum, suggested)
959
+ ];
960
+ const nonEmptyIssues = fromReadonlyArray(issues);
961
+ if (nonEmptyIssues.ok) return err(nonEmptyIssues.value);
962
+ if (!minimum.ok) return err(oneIssue({
963
+ kind: "invalid_amount",
964
+ field: "slidingMinAmount",
965
+ error: minimum.error
966
+ }));
967
+ if (!maximum.ok) return err(oneIssue({
968
+ kind: "invalid_amount",
969
+ field: "slidingMaxAmount",
970
+ error: maximum.error
971
+ }));
972
+ if (!suggested.ok) return err(oneIssue({
973
+ kind: "invalid_amount",
974
+ field: "slidingSuggestedAmount",
975
+ error: suggested.error
976
+ }));
977
+ return ok({
978
+ kind: "sliding_scale",
979
+ currency,
980
+ minimum: minimum.value,
981
+ maximum: maximum.value,
982
+ suggested: suggested.value
983
+ });
984
+ }
985
+ function parseEventPricing(input) {
986
+ if (!isEventPricingStrategy(input.pricingStrategy)) {
987
+ return err(oneIssue({
988
+ kind: "invalid_strategy",
989
+ value: input.pricingStrategy
990
+ }));
991
+ }
992
+ const currency = typeof input.currency === "string" ? parseCurrencyCode(input.currency) : null;
993
+ if (currency === null) return err(oneIssue({
994
+ kind: "invalid_currency",
995
+ value: input.currency
996
+ }));
997
+ switch (input.pricingStrategy) {
998
+ case "free": return parseFreePricing(input, currency);
999
+ case "single": return parseSinglePricing(input, currency);
1000
+ case "tiered": return parseTieredPricing(input, currency);
1001
+ case "sliding_scale": return parseSlidingPricing(input, currency);
1002
+ default: return assertNever(input.pricingStrategy);
1003
+ }
1004
+ }
1005
+ function isEventPricingStrategy(value) {
1006
+ return EVENT_PRICING_STRATEGIES.some((strategy) => strategy === value);
1007
+ }
1008
+
1009
+ //#endregion
1010
+ //#region ../core/src/planner.ts
1011
+ function plannerNoChange(details = {}) {
1012
+ return {
1013
+ ...details,
1014
+ tag: "no_change"
1015
+ };
1016
+ }
1017
+ function plannerUpdate(details = {}) {
1018
+ return {
1019
+ ...details,
1020
+ tag: "update"
1021
+ };
1022
+ }
1023
+ /** Identity at runtime; marks a planner-derived patch as shallow readonly. */
1024
+ function plannerPatch(patch) {
1025
+ return patch;
1026
+ }
1027
+ /** Default clock for production callers that do not inject time explicitly. */
1028
+ const systemPlanningClock = { now: () => Date.now() };
1029
+ /** Resolve current time from an explicit timestamp, injected clock, or system clock. */
1030
+ function resolvePlanningNow(input = {}) {
1031
+ if (typeof input.now === "number") {
1032
+ return input.now;
1033
+ }
1034
+ return (input.clock ?? systemPlanningClock).now();
1035
+ }
1036
+ /** Resolve current time as a Date from an explicit Date, injected clock, or system clock. */
1037
+ function resolvePlanningNowDate(input = {}) {
1038
+ if (input.now !== undefined) {
1039
+ return input.now;
1040
+ }
1041
+ return new Date((input.clock ?? systemPlanningClock).now());
1042
+ }
1043
+ /**
1044
+ * Resolve a Date-compatible planner input.
1045
+ *
1046
+ * Precedence: bare Date, `{ now }`, `{ clock }`, then `systemPlanningClock`.
1047
+ * Explicit Date inputs are returned by reference, not cloned.
1048
+ */
1049
+ function resolvePlanningDateInput(input = {}) {
1050
+ if (input instanceof Date) {
1051
+ return input;
1052
+ }
1053
+ return resolvePlanningNowDate(input);
1054
+ }
1055
+ /** Build an enabled selection while preserving literal value types. */
1056
+ function enabledSelection(value) {
1057
+ return {
1058
+ enabled: true,
1059
+ value
1060
+ };
1061
+ }
1062
+ /** Build a disabled selection. */
1063
+ function disabledSelection() {
1064
+ return { enabled: false };
1065
+ }
1066
+
1067
+ //#endregion
1068
+ //#region ../core/src/event-pricing.ts
1069
+ /** Capacity closes booking, but does not make an otherwise eligible price unknown. */
1070
+ function isPublicEventPricingOptionDisplayable(option) {
1071
+ if (!option.isActive) return false;
1072
+ if (option.availability.kind === "available") return true;
1073
+ const reason = option.availability.reason;
1074
+ switch (reason) {
1075
+ case "sold_out":
1076
+ case "overallCapacityUnavailable": return true;
1077
+ case "inactive":
1078
+ case "not_on_sale_yet":
1079
+ case "sale_ended":
1080
+ case "attendance_mode_unavailable": return false;
1081
+ default: return assertNever(reason);
1082
+ }
1083
+ }
1084
+ function publicEventPricingDisplay(pricing) {
1085
+ switch (pricing.kind) {
1086
+ case "free":
1087
+ case "single":
1088
+ case "sliding_scale": return canonicalEventPricingDisplay(pricing);
1089
+ case "tiered": return resolvePricingOptionsPriceDisplay({
1090
+ options: pricing.options.filter(isPublicEventPricingOptionDisplayable),
1091
+ currency: pricing.currency,
1092
+ purpose: "configured"
1093
+ });
1094
+ case "external": return {
1095
+ kind: "external",
1096
+ label: pricing.label
1097
+ };
1098
+ }
1099
+ }
1100
+ function publicEventPricingType(pricing) {
1101
+ switch (pricing.kind) {
1102
+ case "free": return "free";
1103
+ case "single":
1104
+ case "tiered":
1105
+ case "sliding_scale": return isFreePublicEventPriceDisplay(publicEventPricingDisplay(pricing)) ? "free" : "paid";
1106
+ case "external": return "external";
1107
+ }
1108
+ }
1109
+ function publicEventPricingCurrency(pricing) {
1110
+ switch (pricing.kind) {
1111
+ case "free":
1112
+ case "single":
1113
+ case "tiered":
1114
+ case "sliding_scale": return pricing.currency;
1115
+ case "external": return null;
1116
+ }
1117
+ }
1118
+ function publicEventScalarPrice(pricing) {
1119
+ return pricing.kind === "single" ? pricing.amount : null;
1120
+ }
1121
+ function publicEventPricingStrategy(pricing) {
1122
+ switch (pricing.kind) {
1123
+ case "single": return "single";
1124
+ case "free": return "free";
1125
+ case "tiered": return "tiered";
1126
+ case "sliding_scale": return "sliding_scale";
1127
+ case "external": return undefined;
1128
+ }
1129
+ }
1130
+ function publicEventPricingOptions(pricing) {
1131
+ return pricing.kind === "tiered" ? pricing.options : [];
1132
+ }
1133
+ function isFreePublicEventPriceDisplay(display) {
1134
+ switch (display.kind) {
1135
+ case "free": return true;
1136
+ case "exact":
1137
+ case "from":
1138
+ case "range":
1139
+ case "mixed":
1140
+ case "unavailable":
1141
+ case "external": return false;
1142
+ }
1143
+ }
1144
+ function resolvePricingOptionSaleWindowState(option, now) {
1145
+ if (option.saleStartsAt !== null && option.saleStartsAt !== undefined && new Date(option.saleStartsAt) > now) {
1146
+ return "not_started";
1147
+ }
1148
+ if (option.saleEndsAt !== null && option.saleEndsAt !== undefined && new Date(option.saleEndsAt) < now) {
1149
+ return "ended";
1150
+ }
1151
+ return "open";
1152
+ }
1153
+ function resolveExactOrFreePriceDisplay(amount, currency) {
1154
+ const resolvedAmount = amount ?? 0;
1155
+ return resolvedAmount > 0 ? {
1156
+ kind: "exact",
1157
+ amount: resolvedAmount,
1158
+ currency
1159
+ } : {
1160
+ kind: "free",
1161
+ currency
1162
+ };
1163
+ }
1164
+ function resolvePricingOptionsPriceDisplay(input) {
1165
+ const now = input.purpose === "available_now" ? resolvePlanningNowDate({
1166
+ now: input.now,
1167
+ clock: input.clock
1168
+ }) : null;
1169
+ const amounts = input.options.filter((option) => option.isActive && (now === null || resolvePricingOptionSaleWindowState(option, now) === "open")).map((option) => option.priceAmount);
1170
+ return amounts.length === 0 ? {
1171
+ kind: "unavailable",
1172
+ currency: input.currency
1173
+ } : amountRangeDisplay(amounts, input.currency);
1174
+ }
1175
+ function amountRangeDisplay(amounts, currency) {
1176
+ const minAmount = Math.min(...amounts);
1177
+ const maxAmount = Math.max(...amounts);
1178
+ if (minAmount === maxAmount) {
1179
+ return minAmount === 0 ? {
1180
+ kind: "free",
1181
+ currency
1182
+ } : {
1183
+ kind: "exact",
1184
+ amount: minAmount,
1185
+ currency
1186
+ };
1187
+ }
1188
+ if (minAmount === 0) {
1189
+ const paidAmounts = amounts.filter((amount) => amount > 0);
1190
+ return {
1191
+ kind: "mixed",
1192
+ paidFromAmount: Math.min(...paidAmounts),
1193
+ paidToAmount: maxAmount,
1194
+ currency
1195
+ };
1196
+ }
1197
+ return {
1198
+ kind: "range",
1199
+ minAmount,
1200
+ maxAmount,
1201
+ currency
1202
+ };
1203
+ }
1204
+ /** Derives configured price presentation directly from canonical pricing. */
1205
+ function canonicalEventPricingDisplay(pricing) {
1206
+ switch (pricing.kind) {
1207
+ case "free": return {
1208
+ kind: "free",
1209
+ currency: pricing.currency
1210
+ };
1211
+ case "single": return {
1212
+ kind: "exact",
1213
+ amount: pricing.amount,
1214
+ currency: pricing.currency
1215
+ };
1216
+ case "sliding_scale": return pricing.minimum === pricing.maximum ? {
1217
+ kind: "exact",
1218
+ amount: pricing.maximum,
1219
+ currency: pricing.currency
1220
+ } : {
1221
+ kind: "range",
1222
+ minAmount: pricing.minimum,
1223
+ maxAmount: pricing.maximum,
1224
+ currency: pricing.currency
1225
+ };
1226
+ case "tiered": {
1227
+ return resolvePricingOptionsPriceDisplay({
1228
+ options: pricing.options,
1229
+ currency: pricing.currency,
1230
+ purpose: "configured"
1231
+ });
1232
+ }
1233
+ default: return assertNever(pricing);
1234
+ }
1235
+ }
1236
+ /**
1237
+ * The single renderer for a precomputed booking price `display` ADT. This is
1238
+ * the one place booking price variants map to user-facing strings — every call
1239
+ * site (booking entry, events calendar, event/course details, listings, portal)
1240
+ * should route through here so labels never drift. Adding a variant to the ADT
1241
+ * breaks compilation here (via `assertNever`) until it is handled.
1242
+ */
1243
+ function formatBookingPriceDisplay(display, options = {}) {
1244
+ switch (display.kind) {
1245
+ case "free": return "Free";
1246
+ case "exact": return formatCurrencyAmount(display.amount, display.currency, options);
1247
+ case "from": return `From ${formatCurrencyAmount(display.amount, display.currency, options)}`;
1248
+ case "range": return `${formatCurrencyAmount(display.minAmount, display.currency, options)} - ${formatCurrencyAmount(display.maxAmount, display.currency, options)}`;
1249
+ case "mixed": return display.paidToAmount > display.paidFromAmount ? `Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)} - ${formatCurrencyAmount(display.paidToAmount, display.currency, options)}` : `Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)}`;
1250
+ case "unavailable": return "Pricing unavailable";
1251
+ case "external": return display.label;
1252
+ default: return assertNever(display);
1253
+ }
1254
+ }
1255
+ /** Render an event's price directly from its pricing value. */
1256
+ function formatEventPricing(pricing, options = {}) {
1257
+ return formatBookingPriceDisplay(publicEventPricingDisplay(pricing), options);
1258
+ }
1259
+
1260
+ //#endregion
1261
+ //#region ../core/src/event-page-surface-subject.ts
1262
+ function createEventSeriesPageSurfaceSubject(eventSeriesId, bookingScope, eventDestinationMode) {
1263
+ const mode = eventDestinationMode ? { eventDestinationMode } : {};
1264
+ return bookingScope === "whole_series" ? {
1265
+ kind: "eventSeries",
1266
+ eventSeriesId,
1267
+ bookingScope,
1268
+ isWholeSeriesBooking: true,
1269
+ ...mode
1270
+ } : {
1271
+ kind: "eventSeries",
1272
+ eventSeriesId,
1273
+ bookingScope,
1274
+ ...mode
1275
+ };
1276
+ }
1277
+ function createEventOfferingPageSurfaceSubject(eventOfferingId, input) {
1278
+ const mode = input.eventDestinationMode ? { eventDestinationMode: input.eventDestinationMode } : {};
1279
+ const identity = {
1280
+ kind: "eventOffering",
1281
+ eventOfferingId,
1282
+ catalogueEventSeriesIds: input.catalogueEventSeriesIds
1283
+ };
1284
+ return input.bookingScope === "whole_series" ? {
1285
+ ...identity,
1286
+ bookingScope: input.bookingScope,
1287
+ isWholeSeriesBooking: true,
1288
+ ...mode
1289
+ } : {
1290
+ ...identity,
1291
+ bookingScope: input.bookingScope,
1292
+ ...mode
1293
+ };
1294
+ }
1295
+ /**
1296
+ * Read the destination mode off a wire value.
1297
+ *
1298
+ * Absent or unrecognised reads as absent, which leaves the routed Event record
1299
+ * as the only authority — the behaviour every caller had before the field
1300
+ * existed.
1301
+ */
1302
+ function parseEventPageSurfaceDestinationMode(value) {
1303
+ return value === "cms-managed" || value === "system-managed" ? value : undefined;
1304
+ }
1305
+ function parseEventPageSurfaceSubject(value) {
1306
+ if (!isRecord$8(value)) return err({ kind: "notObject" });
1307
+ switch (value.kind) {
1308
+ case "eventSeries": {
1309
+ const eventSeriesId = parseEventSeriesId(value.eventSeriesId);
1310
+ if (!eventSeriesId.ok) {
1311
+ return err({
1312
+ kind: "invalidEventSeriesId",
1313
+ error: eventSeriesId.error
1314
+ });
1315
+ }
1316
+ if (value.bookingScope !== "single_occurrence" && value.bookingScope !== "whole_series") {
1317
+ return err({
1318
+ kind: "invalidEventSeriesBookingScope",
1319
+ bookingScope: value.bookingScope
1320
+ });
1321
+ }
1322
+ return ok(createEventSeriesPageSurfaceSubject(eventSeriesId.value, value.bookingScope, parseEventPageSurfaceDestinationMode(value.eventDestinationMode)));
1323
+ }
1324
+ case "eventOffering": {
1325
+ const eventOfferingId = parseEventOfferingId(value.eventOfferingId);
1326
+ if (!eventOfferingId.ok) {
1327
+ return err({
1328
+ kind: "invalidEventOfferingId",
1329
+ error: eventOfferingId.error
1330
+ });
1331
+ }
1332
+ if (!Array.isArray(value.catalogueEventSeriesIds)) {
1333
+ return err({
1334
+ kind: "invalidEventOfferingCatalogue",
1335
+ catalogue: value.catalogueEventSeriesIds
1336
+ });
1337
+ }
1338
+ if (value.bookingScope !== "single_occurrence" && value.bookingScope !== "whole_series") {
1339
+ return err({
1340
+ kind: "invalidEventOfferingBookingScope",
1341
+ bookingScope: value.bookingScope
1342
+ });
1343
+ }
1344
+ const parsedCatalogueIds = parseEventSeriesIdList(value.catalogueEventSeriesIds);
1345
+ if (!parsedCatalogueIds.ok) return parsedCatalogueIds;
1346
+ const eventDestinationMode = parseEventPageSurfaceDestinationMode(value.eventDestinationMode);
1347
+ return ok(createEventOfferingPageSurfaceSubject(eventOfferingId.value, {
1348
+ catalogueEventSeriesIds: parsedCatalogueIds.value,
1349
+ bookingScope: value.bookingScope,
1350
+ ...eventDestinationMode ? { eventDestinationMode } : {}
1351
+ }));
1352
+ }
1353
+ default: return err({
1354
+ kind: "unsupportedSubject",
1355
+ subjectKind: value.kind
1356
+ });
1357
+ }
1358
+ }
1359
+ function toEventPageSurfaceSubjectWire(subject) {
1360
+ return subject;
1361
+ }
1362
+ function parseEventSeriesIdList(values) {
1363
+ const parsed = [];
1364
+ for (const value of values) {
1365
+ const eventSeriesId = parseEventSeriesId(value);
1366
+ if (!eventSeriesId.ok) {
1367
+ return err({
1368
+ kind: "invalidEventSeriesId",
1369
+ error: eventSeriesId.error
1370
+ });
1371
+ }
1372
+ parsed.push(eventSeriesId.value);
1373
+ }
1374
+ return ok(parsed);
1375
+ }
1376
+ function isRecord$8(value) {
1377
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1378
+ }
1379
+
846
1380
  //#endregion
847
1381
  //#region ../core/src/booking-flexible-balance.ts
848
1382
  const DEFAULT_FLEXIBLE_BALANCE_STRIPE_MINIMUM_CHARGE_CENTS = 50;
@@ -1824,19 +2358,6 @@ function unwrapNarrow(result) {
1824
2358
  return result.value;
1825
2359
  }
1826
2360
 
1827
- //#endregion
1828
- //#region ../core/src/non-empty-array.ts
1829
- function fromArray(items) {
1830
- const [first, ...rest] = items;
1831
- if (first === undefined) {
1832
- return err("empty_array");
1833
- }
1834
- return ok([first, ...rest]);
1835
- }
1836
- function mapNonEmptyArray(items, map) {
1837
- return items.map(map);
1838
- }
1839
-
1840
2361
  //#endregion
1841
2362
  //#region ../core/src/editor-state.ts
1842
2363
  const unsafeAsEditorSiteId = (value) => value;
@@ -2718,7 +3239,7 @@ const knownSocialPlatforms = [
2718
3239
  "x",
2719
3240
  "youtube"
2720
3241
  ];
2721
- function isRecord$5(value) {
3242
+ function isRecord$7(value) {
2722
3243
  return typeof value === "object" && value !== null && !Array.isArray(value);
2723
3244
  }
2724
3245
  function trimmedOptionalString(value) {
@@ -2728,7 +3249,7 @@ function trimmedOptionalString(value) {
2728
3249
  return trimmed.length > 0 ? trimmed : null;
2729
3250
  }
2730
3251
  function parsePublicContactDetails(value) {
2731
- if (!isRecord$5(value)) {
3252
+ if (!isRecord$7(value)) {
2732
3253
  return err({
2733
3254
  code: "invalid_shape",
2734
3255
  path: "contact",
@@ -2824,7 +3345,7 @@ function minutesSinceMidnight(value) {
2824
3345
  }
2825
3346
  function parseOpeningHoursDay(value, expectedWeekday, index) {
2826
3347
  const path = `openingHours.days.${index}`;
2827
- if (!isRecord$5(value) || value.weekday !== expectedWeekday) {
3348
+ if (!isRecord$7(value) || value.weekday !== expectedWeekday) {
2828
3349
  return err({
2829
3350
  code: "invalid_weekdays",
2830
3351
  path,
@@ -2845,7 +3366,7 @@ function parseOpeningHoursDay(value, expectedWeekday, index) {
2845
3366
  const intervals = [];
2846
3367
  for (const [intervalIndex, rawInterval] of value.intervals.entries()) {
2847
3368
  const intervalPath = `${path}.intervals.${intervalIndex}`;
2848
- if (!isRecord$5(rawInterval)) {
3369
+ if (!isRecord$7(rawInterval)) {
2849
3370
  return err({
2850
3371
  code: "invalid_interval",
2851
3372
  path: intervalPath,
@@ -2895,7 +3416,7 @@ function parseOpeningHoursDay(value, expectedWeekday, index) {
2895
3416
  });
2896
3417
  }
2897
3418
  function parseWeeklyOpeningHours(value) {
2898
- if (!isRecord$5(value) || !Array.isArray(value.days) || value.days.length !== publicProfileWeekdays.length) {
3419
+ if (!isRecord$7(value) || !Array.isArray(value.days) || value.days.length !== publicProfileWeekdays.length) {
2899
3420
  return err({
2900
3421
  code: "invalid_weekdays",
2901
3422
  path: "openingHours.days",
@@ -2975,7 +3496,7 @@ function parsePublicSocialLinks(value) {
2975
3496
  const ids = new Set();
2976
3497
  for (const [index, rawLink] of value.entries()) {
2977
3498
  const path = `socialLinks.${index}`;
2978
- if (!isRecord$5(rawLink)) {
3499
+ if (!isRecord$7(rawLink)) {
2979
3500
  return err({
2980
3501
  code: "invalid_social_link",
2981
3502
  path,
@@ -3022,7 +3543,7 @@ function parsePublicSocialLinks(value) {
3022
3543
  return ok(links);
3023
3544
  }
3024
3545
  function parsePublicProfile(value) {
3025
- if (!isRecord$5(value)) {
3546
+ if (!isRecord$7(value)) {
3026
3547
  return err({
3027
3548
  code: "invalid_shape",
3028
3549
  path: "",
@@ -3035,23 +3556,36 @@ function parsePublicProfile(value) {
3035
3556
  if (!openingHours.ok) return openingHours;
3036
3557
  const socialLinks = parsePublicSocialLinks(value.socialLinks);
3037
3558
  if (!socialLinks.ok) return socialLinks;
3559
+ const addressVisibility = parseAddressVisibility(value.addressVisibility);
3560
+ if (!addressVisibility.ok) return addressVisibility;
3038
3561
  return ok({
3039
3562
  contact: contact.value,
3040
3563
  openingHours: openingHours.value,
3041
- socialLinks: socialLinks.value
3564
+ socialLinks: socialLinks.value,
3565
+ addressVisibility: addressVisibility.value
3566
+ });
3567
+ }
3568
+ function parseAddressVisibility(value) {
3569
+ if (value === undefined || value === null) return ok("shown");
3570
+ if (value === "shown" || value === "hidden") return ok(value);
3571
+ return err({
3572
+ code: "invalid_address_visibility",
3573
+ path: "addressVisibility",
3574
+ message: "Address visibility must be shown or hidden."
3042
3575
  });
3043
3576
  }
3044
3577
  function emptyPublicProfile() {
3045
3578
  return {
3046
3579
  contact: null,
3047
3580
  openingHours: null,
3048
- socialLinks: []
3581
+ socialLinks: [],
3582
+ addressVisibility: "shown"
3049
3583
  };
3050
3584
  }
3051
3585
 
3052
3586
  //#endregion
3053
3587
  //#region ../core/src/rich-text.ts
3054
- function isRecord$4(value) {
3588
+ function isRecord$6(value) {
3055
3589
  return typeof value === "object" && value !== null && !Array.isArray(value);
3056
3590
  }
3057
3591
  function hasOnlyKeys(value, allowed) {
@@ -3073,10 +3607,10 @@ function isDenseArrayOf(value, predicate) {
3073
3607
  return true;
3074
3608
  }
3075
3609
  function isTipTapMark(value) {
3076
- return isRecord$4(value) && hasOnlyKeys(value, markKeys) && typeof value.type === "string" && value.type.length > 0 && (value.attrs === undefined || isRecord$4(value.attrs));
3610
+ return isRecord$6(value) && hasOnlyKeys(value, markKeys) && typeof value.type === "string" && value.type.length > 0 && (value.attrs === undefined || isRecord$6(value.attrs));
3077
3611
  }
3078
3612
  function isTipTapNode(value) {
3079
- if (!isRecord$4(value) || !hasOnlyKeys(value, nodeKeys) || typeof value.type !== "string" || value.type.length === 0 || value.type === "doc" || value.attrs !== undefined && !isRecord$4(value.attrs) || value.marks !== undefined && (!Array.isArray(value.marks) || !isDenseArrayOf(value.marks, isTipTapMark)) || value.content !== undefined && (!Array.isArray(value.content) || !isDenseArrayOf(value.content, isTipTapNode))) {
3613
+ if (!isRecord$6(value) || !hasOnlyKeys(value, nodeKeys) || typeof value.type !== "string" || value.type.length === 0 || value.type === "doc" || value.attrs !== undefined && !isRecord$6(value.attrs) || value.marks !== undefined && (!Array.isArray(value.marks) || !isDenseArrayOf(value.marks, isTipTapMark)) || value.content !== undefined && (!Array.isArray(value.content) || !isDenseArrayOf(value.content, isTipTapNode))) {
3080
3614
  return false;
3081
3615
  }
3082
3616
  if (value.type === "text") {
@@ -3085,89 +3619,222 @@ function isTipTapNode(value) {
3085
3619
  return value.text === undefined;
3086
3620
  }
3087
3621
  function isCanonicalRichTextDoc(value) {
3088
- return isRecord$4(value) && hasOnlyKeys(value, rootKeys) && value.type === "doc" && Array.isArray(value.content) && isDenseArrayOf(value.content, isTipTapNode);
3622
+ return isRecord$6(value) && hasOnlyKeys(value, rootKeys) && value.type === "doc" && Array.isArray(value.content) && isDenseArrayOf(value.content, isTipTapNode);
3089
3623
  }
3090
3624
 
3091
3625
  //#endregion
3092
- //#region ../core/src/newsletter-signup-destination.ts
3626
+ //#region ../core/src/mailing-list-identifiers.ts
3093
3627
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
3094
- const MAILCHIMP_AUDIENCE_ID_PATTERN = /^[A-Za-z0-9_-]{1,100}$/u;
3628
+ const OPAQUE_ID_PATTERN = /^[A-Za-z0-9_-]{1,100}$/u;
3095
3629
  const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
3096
- function isRecord$3(value) {
3097
- return typeof value === "object" && value !== null && !Array.isArray(value);
3098
- }
3099
- function parseTrimmedString(value) {
3630
+ function parseTrimmedMatchingString(value, pattern) {
3100
3631
  if (typeof value !== "string") return null;
3101
3632
  const trimmed = value.trim();
3102
- return trimmed.length > 0 ? trimmed : null;
3633
+ return pattern.test(trimmed) ? trimmed : null;
3634
+ }
3635
+ function parsePositiveInteger(value) {
3636
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
3103
3637
  }
3104
3638
  function parseNewsletterListId(value) {
3105
- const trimmed = parseTrimmedString(value);
3106
- return trimmed !== null && UUID_PATTERN.test(trimmed) ? trimmed : null;
3639
+ return parseTrimmedMatchingString(value, UUID_PATTERN);
3107
3640
  }
3108
3641
  function parseMailchimpConnectionId(value) {
3109
- const trimmed = parseTrimmedString(value);
3110
- return trimmed !== null && UUID_PATTERN.test(trimmed) ? trimmed : null;
3642
+ return parseTrimmedMatchingString(value, UUID_PATTERN);
3643
+ }
3644
+ function parseMailchimpAccountId(value) {
3645
+ return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
3111
3646
  }
3112
3647
  function parseMailchimpAudienceId(value) {
3113
- const trimmed = parseTrimmedString(value);
3114
- return trimmed !== null && MAILCHIMP_AUDIENCE_ID_PATTERN.test(trimmed) ? trimmed : null;
3648
+ return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
3649
+ }
3650
+ function parseMailchimpInterestId(value) {
3651
+ return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
3652
+ }
3653
+ function parseMailchimpTagName(value) {
3654
+ if (typeof value !== "string") return null;
3655
+ const trimmed = value.trim();
3656
+ return trimmed.length > 0 && trimmed.length <= 100 && !CONTROL_CHARACTER_PATTERN.test(trimmed) ? trimmed : null;
3657
+ }
3658
+ function parseBrevoConnectionId(value) {
3659
+ return parseTrimmedMatchingString(value, UUID_PATTERN);
3660
+ }
3661
+ function parseBrevoAccountId(value) {
3662
+ return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
3663
+ }
3664
+ function parseBrevoListId(value) {
3665
+ return parsePositiveInteger(value);
3666
+ }
3667
+ function parseBrevoConfirmationTemplateId(value) {
3668
+ return parsePositiveInteger(value);
3115
3669
  }
3116
- function parseMailchimpTagName(value) {
3117
- const trimmed = parseTrimmedString(value);
3118
- return trimmed !== null && trimmed.length <= 100 && !CONTROL_CHARACTER_PATTERN.test(trimmed) ? trimmed : null;
3119
- }
3120
- function parseNewsletterSignupDestinationProvider(value) {
3121
- return value === "riverbank" || value === "mailchimp" ? value : null;
3122
- }
3123
- function parseNewsletterSignupAuthoringDestinationValue(value) {
3124
- if (!isRecord$3(value) || typeof value.tag !== "string") {
3125
- return err({ kind: "invalid-destination-shape" });
3126
- }
3127
- switch (value.tag) {
3128
- case "riverbank_list": {
3129
- const listId = parseNewsletterListId(value.listId);
3130
- return listId === null ? err({ kind: "invalid-newsletter-list-id" }) : ok({
3131
- tag: "riverbank_list",
3132
- listId
3670
+
3671
+ //#endregion
3672
+ //#region ../core/src/mailing-list-targeting-parser.ts
3673
+ function parseMailingListPlacementTargeting(value) {
3674
+ if (!isRecord$5(value)) return targetingError("shape");
3675
+ if (value.mode === "default") return ok({ mode: "default" });
3676
+ if (value.mode !== "custom") return targetingError("mode");
3677
+ if (!isRecord$5(value.selection)) return targetingError("selection");
3678
+ switch (value.provider) {
3679
+ case "native": return parseNativeTargeting(value.selection);
3680
+ case "brevo": return parseBrevoTargeting(value.selection);
3681
+ case "mailchimp": return parseMailchimpTargeting(value, value.selection);
3682
+ default: return targetingError("provider");
3683
+ }
3684
+ }
3685
+ function parseMailingListVisitorSelection(value) {
3686
+ if (!isRecord$5(value)) return visitorSelectionError("shape");
3687
+ switch (value.provider) {
3688
+ case "native": {
3689
+ const topicIds = parseVisitorIds(value.topicIds, parseNewsletterListId);
3690
+ return topicIds === null ? visitorSelectionError("targets") : ok({
3691
+ provider: "native",
3692
+ topicIds
3133
3693
  });
3134
3694
  }
3135
- case "mailchimp_audience": {
3136
- const connectionId = parseMailchimpConnectionId(value.connectionId);
3137
- if (connectionId === null) {
3138
- return err({ kind: "invalid-mailchimp-connection-id" });
3695
+ case "brevo": {
3696
+ const listIds = parseVisitorIds(value.listIds, parseBrevoListId);
3697
+ return listIds === null ? visitorSelectionError("targets") : ok({
3698
+ provider: "brevo",
3699
+ listIds
3700
+ });
3701
+ }
3702
+ case "mailchimp": {
3703
+ const interestIds = parseVisitorIds(value.interestIds, parseMailchimpInterestId);
3704
+ return interestIds === null ? visitorSelectionError("targets") : ok({
3705
+ provider: "mailchimp",
3706
+ interestIds
3707
+ });
3708
+ }
3709
+ default: return visitorSelectionError("provider");
3710
+ }
3711
+ }
3712
+ function parseNativeTargeting(selection) {
3713
+ if (selection.tag === "fixed") {
3714
+ const topicIds = parseNonEmptyUniqueIds(selection.topicIds, parseNewsletterListId);
3715
+ return topicIds.ok ? ok({
3716
+ mode: "custom",
3717
+ provider: "native",
3718
+ selection: {
3719
+ tag: "fixed",
3720
+ topicIds: topicIds.value
3139
3721
  }
3140
- const audienceId = parseMailchimpAudienceId(value.audienceId);
3141
- if (audienceId === null) {
3142
- return err({ kind: "invalid-mailchimp-audience-id" });
3722
+ }) : topicIds;
3723
+ }
3724
+ if (selection.tag === "visitor_choice") {
3725
+ const offeredTopicIds = parseNonEmptyUniqueIds(selection.offeredTopicIds, parseNewsletterListId);
3726
+ return offeredTopicIds.ok ? ok({
3727
+ mode: "custom",
3728
+ provider: "native",
3729
+ selection: {
3730
+ tag: "visitor_choice",
3731
+ offeredTopicIds: offeredTopicIds.value
3143
3732
  }
3144
- const rawTags = value.tags ?? [];
3145
- if (!Array.isArray(rawTags)) {
3146
- return err({ kind: "invalid-mailchimp-tags" });
3733
+ }) : offeredTopicIds;
3734
+ }
3735
+ return targetingError("selection");
3736
+ }
3737
+ function parseBrevoTargeting(selection) {
3738
+ if (selection.tag === "fixed") {
3739
+ const listIds = parseNonEmptyUniqueIds(selection.listIds, parseBrevoListId);
3740
+ return listIds.ok ? ok({
3741
+ mode: "custom",
3742
+ provider: "brevo",
3743
+ selection: {
3744
+ tag: "fixed",
3745
+ listIds: listIds.value
3147
3746
  }
3148
- const tags = [];
3149
- for (const valueTag of rawTags) {
3150
- const tag = parseMailchimpTagName(valueTag);
3151
- if (tag === null) return err({ kind: "invalid-mailchimp-tags" });
3152
- tags.push(tag);
3747
+ }) : listIds;
3748
+ }
3749
+ if (selection.tag === "visitor_choice") {
3750
+ const offeredListIds = parseNonEmptyUniqueIds(selection.offeredListIds, parseBrevoListId);
3751
+ return offeredListIds.ok ? ok({
3752
+ mode: "custom",
3753
+ provider: "brevo",
3754
+ selection: {
3755
+ tag: "visitor_choice",
3756
+ offeredListIds: offeredListIds.value
3153
3757
  }
3154
- return ok({
3155
- tag: "mailchimp_audience",
3156
- connectionId,
3157
- audienceId,
3158
- tags
3159
- });
3160
- }
3161
- default: return err({
3162
- kind: "invalid-destination-tag",
3163
- value: value.tag
3164
- });
3758
+ }) : offeredListIds;
3759
+ }
3760
+ return targetingError("selection");
3761
+ }
3762
+ function parseMailchimpTargeting(value, selection) {
3763
+ const audienceId = parseMailchimpAudienceId(value.audienceId);
3764
+ if (audienceId === null) return targetingError("audienceId");
3765
+ if (selection.tag === "fixed") {
3766
+ const interestIds = parseUniqueIds(selection.interestIds, parseMailchimpInterestId);
3767
+ return interestIds.ok ? ok({
3768
+ mode: "custom",
3769
+ provider: "mailchimp",
3770
+ audienceId,
3771
+ selection: {
3772
+ tag: "fixed",
3773
+ interestIds: interestIds.value
3774
+ }
3775
+ }) : interestIds;
3776
+ }
3777
+ if (selection.tag === "visitor_choice") {
3778
+ const offeredInterestIds = parseNonEmptyUniqueIds(selection.offeredInterestIds, parseMailchimpInterestId);
3779
+ return offeredInterestIds.ok ? ok({
3780
+ mode: "custom",
3781
+ provider: "mailchimp",
3782
+ audienceId,
3783
+ selection: {
3784
+ tag: "visitor_choice",
3785
+ offeredInterestIds: offeredInterestIds.value
3786
+ }
3787
+ }) : offeredInterestIds;
3788
+ }
3789
+ return targetingError("selection");
3790
+ }
3791
+ function parseNonEmptyUniqueIds(value, parseId) {
3792
+ const parsed = parseUniqueIds(value, parseId);
3793
+ if (!parsed.ok) return parsed;
3794
+ const [first, ...rest] = parsed.value;
3795
+ return first === undefined ? targetingError("targets") : ok([first, ...rest]);
3796
+ }
3797
+ function parseUniqueIds(value, parseId) {
3798
+ if (!Array.isArray(value)) return targetingError("targets");
3799
+ const parsed = [];
3800
+ for (const item of value) {
3801
+ const id = parseId(item);
3802
+ if (id === null) return targetingError("targets");
3803
+ if (parsed.includes(id)) return targetingError("duplicate_target");
3804
+ parsed.push(id);
3805
+ }
3806
+ return ok(parsed);
3807
+ }
3808
+ function parseVisitorIds(value, parseId) {
3809
+ if (!Array.isArray(value) || value.length === 0) return null;
3810
+ const parsed = [];
3811
+ for (const item of value) {
3812
+ const id = parseId(item);
3813
+ if (id === null) return null;
3814
+ if (parsed.includes(id)) return null;
3815
+ parsed.push(id);
3165
3816
  }
3817
+ return parsed;
3818
+ }
3819
+ function isRecord$5(value) {
3820
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3821
+ }
3822
+ function targetingError(field) {
3823
+ return err({
3824
+ tag: "invalid_mailing_list_targeting",
3825
+ field
3826
+ });
3827
+ }
3828
+ function visitorSelectionError(field) {
3829
+ return err({
3830
+ tag: "invalid_mailing_list_visitor_selection",
3831
+ field
3832
+ });
3166
3833
  }
3167
3834
 
3168
3835
  //#endregion
3169
3836
  //#region ../core/src/footer-engagement.ts
3170
- function isRecord$2(value) {
3837
+ function isRecord$4(value) {
3171
3838
  return typeof value === "object" && value !== null && !Array.isArray(value);
3172
3839
  }
3173
3840
  function invalid(path, message) {
@@ -3201,7 +3868,7 @@ function parseEngagementBase(value, base, path) {
3201
3868
  }
3202
3869
  const treatment = parseTreatment(value.treatment, `${path}.treatment`);
3203
3870
  if (!treatment.ok) return treatment;
3204
- if (!isRecord$2(value.source)) {
3871
+ if (!isRecord$4(value.source)) {
3205
3872
  return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
3206
3873
  }
3207
3874
  const intro = parseBoundedText(value.source.intro, `${path}.source.intro`, {
@@ -3230,36 +3897,17 @@ function parseStandardContact(source, base, path) {
3230
3897
  }
3231
3898
  function parseMailingList(source, base, path) {
3232
3899
  const { intro, ...section } = base;
3233
- const destination = parseNewsletterSignupAuthoringDestinationValue(source.destination);
3234
- if (!destination.ok) {
3235
- return err(invalid(`${path}.destination`, "Footer mailing-list destination is invalid."));
3236
- }
3237
- const buttonLabel = parseBoundedText(source.buttonLabel, `${path}.buttonLabel`, { maxLength: 40 });
3238
- if (!buttonLabel.ok) return buttonLabel;
3239
- const successMessage = parseBoundedText(source.successMessage, `${path}.successMessage`, { maxLength: 160 });
3240
- if (!successMessage.ok) return successMessage;
3241
- const privacyNote = parseBoundedText(source.privacyNote, `${path}.privacyNote`, {
3242
- maxLength: 240,
3243
- optional: true
3244
- });
3245
- if (!privacyNote.ok) return privacyNote;
3246
- if (buttonLabel.value === null || successMessage.value === null) {
3247
- return err(invalid(path, "Footer mailing-list copy cannot be empty."));
3248
- }
3249
- if (source.fieldPreset !== "emailOnly") {
3250
- return err(invalid(`${path}.fieldPreset`, "Footer mailing-list signup must be email-only."));
3900
+ const targeting = parseMailingListPlacementTargeting(source.targeting);
3901
+ if (!targeting.ok) {
3902
+ return err(invalid(`${path}.targeting`, "Footer mailing-list targeting is invalid."));
3251
3903
  }
3252
3904
  return ok({
3253
3905
  ...section,
3254
3906
  kind: "engagement",
3255
3907
  source: {
3256
3908
  kind: "mailing-list",
3257
- destination: destination.value,
3258
- intro,
3259
- buttonLabel: buttonLabel.value,
3260
- successMessage: successMessage.value,
3261
- privacyNote: privacyNote.value,
3262
- fieldPreset: "emailOnly"
3909
+ targeting: targeting.value,
3910
+ intro
3263
3911
  }
3264
3912
  });
3265
3913
  }
@@ -3267,7 +3915,7 @@ function parseFooterEngagementSectionValue(value, sectionBase, path) {
3267
3915
  const base = parseEngagementBase(value, sectionBase, path);
3268
3916
  if (!base.ok) return base;
3269
3917
  const source = value.source;
3270
- if (!isRecord$2(source)) {
3918
+ if (!isRecord$4(source)) {
3271
3919
  return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
3272
3920
  }
3273
3921
  switch (source.kind) {
@@ -3382,7 +4030,7 @@ function unsafeAsFooterModuleId(value) {
3382
4030
  function parseFooterModuleId(value) {
3383
4031
  return parseSectionId(value, "sectionId");
3384
4032
  }
3385
- function isRecord$1(value) {
4033
+ function isRecord$3(value) {
3386
4034
  return typeof value === "object" && value !== null && !Array.isArray(value);
3387
4035
  }
3388
4036
  function isRichText(value) {
@@ -3412,7 +4060,7 @@ function parseHeading(value, path) {
3412
4060
  }
3413
4061
  function parseFooterSection(value, index) {
3414
4062
  const path = `sections.${index}`;
3415
- if (!isRecord$1(value)) {
4063
+ if (!isRecord$3(value)) {
3416
4064
  return err({
3417
4065
  code: "invalid_section",
3418
4066
  path,
@@ -3447,7 +4095,7 @@ function parseFooterSection(value, index) {
3447
4095
  }
3448
4096
  break;
3449
4097
  case "navigation":
3450
- if (isRecord$1(value.source) && value.source.kind === "menu-role" && value.source.role === "main") {
4098
+ if (isRecord$3(value.source) && value.source.kind === "menu-role" && value.source.role === "main") {
3451
4099
  return ok({
3452
4100
  ...base,
3453
4101
  kind: "navigation",
@@ -3460,7 +4108,7 @@ function parseFooterSection(value, index) {
3460
4108
  }
3461
4109
  break;
3462
4110
  case "legal-navigation":
3463
- if (isRecord$1(value.source) && value.source.kind === "menu-role" && value.source.role === "legal-policy") {
4111
+ if (isRecord$3(value.source) && value.source.kind === "menu-role" && value.source.role === "legal-policy") {
3464
4112
  return ok({
3465
4113
  ...base,
3466
4114
  kind: "legal-navigation",
@@ -3473,7 +4121,7 @@ function parseFooterSection(value, index) {
3473
4121
  }
3474
4122
  break;
3475
4123
  case "business-address":
3476
- if (isRecord$1(value.source) && value.source.kind === "business-address") {
4124
+ if (isRecord$3(value.source) && value.source.kind === "business-address") {
3477
4125
  return ok({
3478
4126
  ...base,
3479
4127
  kind: "business-address",
@@ -3485,7 +4133,7 @@ function parseFooterSection(value, index) {
3485
4133
  case "contact":
3486
4134
  case "opening-hours":
3487
4135
  case "social-links":
3488
- if (isRecord$1(value.source) && value.source.kind === "public-profile") {
4136
+ if (isRecord$3(value.source) && value.source.kind === "public-profile") {
3489
4137
  return ok({
3490
4138
  ...base,
3491
4139
  kind: value.kind,
@@ -3537,7 +4185,7 @@ function validateFooterComposition(composition) {
3537
4185
  return ok(composition);
3538
4186
  }
3539
4187
  function parseFooterCompositionV1(value) {
3540
- if (!isRecord$1(value)) {
4188
+ if (!isRecord$3(value)) {
3541
4189
  return err({
3542
4190
  code: "invalid_shape",
3543
4191
  path: "",
@@ -3570,7 +4218,7 @@ function parseFooterCompositionV1(value) {
3570
4218
  });
3571
4219
  }
3572
4220
  function parseLegacyPlacementSection(value, index, selectedIds) {
3573
- if (!isRecord$1(value)) {
4221
+ if (!isRecord$3(value)) {
3574
4222
  return err({
3575
4223
  code: "invalid_section",
3576
4224
  path: `modules.${index}`,
@@ -3578,7 +4226,7 @@ function parseLegacyPlacementSection(value, index, selectedIds) {
3578
4226
  });
3579
4227
  }
3580
4228
  const selected = typeof value.id === "string" && selectedIds.has(value.id);
3581
- const source = isRecord$1(value.source) ? value.source : null;
4229
+ const source = isRecord$3(value.source) ? value.source : null;
3582
4230
  const mapped = value.kind === "navigation" && source?.kind === "menu-role" && source.role === "footer" ? {
3583
4231
  ...value,
3584
4232
  source: {
@@ -3597,7 +4245,7 @@ function parseLegacyPlacementSection(value, index, selectedIds) {
3597
4245
  * It is intentionally not called by the runtime parser.
3598
4246
  */
3599
4247
  function upgradePlacementFooterCompositionV1(value) {
3600
- if (!isRecord$1(value) || value.schemaVersion !== 1 || !Array.isArray(value.modules) || !Array.isArray(value.placements)) {
4248
+ if (!isRecord$3(value) || value.schemaVersion !== 1 || !Array.isArray(value.modules) || !Array.isArray(value.placements)) {
3601
4249
  return err({
3602
4250
  code: "invalid_shape",
3603
4251
  path: "",
@@ -3606,7 +4254,7 @@ function upgradePlacementFooterCompositionV1(value) {
3606
4254
  }
3607
4255
  const selectedIds = new Set();
3608
4256
  for (const [index, placement] of value.placements.entries()) {
3609
- if (!isRecord$1(placement) || typeof placement.moduleId !== "string") {
4257
+ if (!isRecord$3(placement) || typeof placement.moduleId !== "string") {
3610
4258
  return err({
3611
4259
  code: "invalid_shape",
3612
4260
  path: `placements.${index}`,
@@ -3709,8 +4357,8 @@ function repairTargetFor(section) {
3709
4357
  };
3710
4358
  }
3711
4359
  }
3712
- function resolveFooterSections(composition, availability) {
3713
- return composition.sections.filter((section) => section.selected).map((section) => isSectionRenderable(section, availability) ? {
4360
+ function resolveFooterSections(composition, availability$1) {
4361
+ return composition.sections.filter((section) => section.selected).map((section) => isSectionRenderable(section, availability$1) ? {
3714
4362
  kind: "renderable",
3715
4363
  section
3716
4364
  } : {
@@ -3719,127 +4367,837 @@ function resolveFooterSections(composition, availability) {
3719
4367
  repairTarget: repairTargetFor(section)
3720
4368
  });
3721
4369
  }
3722
- function isSectionRenderable(section, availability) {
3723
- switch (section.kind) {
3724
- case "brand": return section.showLogo && availability.hasLogo || section.showTitle;
3725
- case "navigation": return availability.hasMainNavigation;
3726
- case "legal-navigation": return availability.hasLegalPolicyNavigation;
3727
- case "business-address": return availability.hasBusinessAddress;
3728
- case "contact": return availability.hasContact;
3729
- case "opening-hours": return availability.hasOpeningHours;
3730
- case "social-links": return availability.hasSocialLinks;
3731
- case "engagement": return availability.hasEngagementSource;
3732
- case "supporting-text":
3733
- case "legal": return section.content !== null;
4370
+ function isSectionRenderable(section, availability$1) {
4371
+ switch (section.kind) {
4372
+ case "brand": return section.showLogo && availability$1.hasLogo || section.showTitle;
4373
+ case "navigation": return availability$1.hasMainNavigation;
4374
+ case "legal-navigation": return availability$1.hasLegalPolicyNavigation;
4375
+ case "business-address": return availability$1.hasBusinessAddress;
4376
+ case "contact": return availability$1.hasContact;
4377
+ case "opening-hours": return availability$1.hasOpeningHours;
4378
+ case "social-links": return availability$1.hasSocialLinks;
4379
+ case "engagement": return availability$1.hasEngagementSource;
4380
+ case "supporting-text":
4381
+ case "legal": return section.content !== null;
4382
+ }
4383
+ }
4384
+ function deriveFooterEditorOverview(composition) {
4385
+ return {
4386
+ selected: composition.sections.filter((section) => section.selected).map((section) => ({
4387
+ section,
4388
+ status: "selected"
4389
+ })),
4390
+ available: composition.sections.filter((section) => !section.selected).map((section) => ({
4391
+ section,
4392
+ status: "available"
4393
+ }))
4394
+ };
4395
+ }
4396
+ const defaultSections = (legacy, options) => {
4397
+ const footerVisible = options.footerVisible ?? true;
4398
+ return [
4399
+ {
4400
+ id: unsafeAsFooterModuleId("brand"),
4401
+ kind: "brand",
4402
+ heading: null,
4403
+ selected: footerVisible,
4404
+ showLogo: options.brand?.showLogo ?? true,
4405
+ showTitle: options.brand?.showTitle ?? true
4406
+ },
4407
+ {
4408
+ id: unsafeAsFooterModuleId("navigation"),
4409
+ kind: "navigation",
4410
+ heading: null,
4411
+ selected: footerVisible,
4412
+ source: {
4413
+ kind: "menu-role",
4414
+ role: "main"
4415
+ }
4416
+ },
4417
+ {
4418
+ id: unsafeAsFooterModuleId("legal-navigation"),
4419
+ kind: "legal-navigation",
4420
+ heading: null,
4421
+ selected: false,
4422
+ source: {
4423
+ kind: "menu-role",
4424
+ role: "legal-policy"
4425
+ }
4426
+ },
4427
+ {
4428
+ id: unsafeAsFooterModuleId("business-address"),
4429
+ kind: "business-address",
4430
+ heading: null,
4431
+ selected: footerVisible && legacy.showBusinessAddress === true,
4432
+ source: { kind: "business-address" }
4433
+ },
4434
+ {
4435
+ id: unsafeAsFooterModuleId("contact"),
4436
+ kind: "contact",
4437
+ heading: null,
4438
+ selected: false,
4439
+ source: { kind: "public-profile" }
4440
+ },
4441
+ {
4442
+ id: unsafeAsFooterModuleId("opening-hours"),
4443
+ kind: "opening-hours",
4444
+ heading: null,
4445
+ selected: false,
4446
+ source: { kind: "public-profile" }
4447
+ },
4448
+ {
4449
+ id: unsafeAsFooterModuleId("social-links"),
4450
+ kind: "social-links",
4451
+ heading: null,
4452
+ selected: false,
4453
+ source: { kind: "public-profile" }
4454
+ },
4455
+ {
4456
+ id: unsafeAsFooterModuleId("supporting-text"),
4457
+ kind: "supporting-text",
4458
+ heading: null,
4459
+ selected: footerVisible && isRichText(legacy.bodyText),
4460
+ content: isRichText(legacy.bodyText) ? legacy.bodyText : null
4461
+ },
4462
+ {
4463
+ id: unsafeAsFooterModuleId("legal"),
4464
+ kind: "legal",
4465
+ heading: null,
4466
+ selected: footerVisible && isRichText(legacy.bottomText),
4467
+ content: isRichText(legacy.bottomText) ? legacy.bottomText : null
4468
+ }
4469
+ ];
4470
+ };
4471
+ function normalizeLegacyFooterContentToV1(value, options = {}) {
4472
+ if (!isRecord$3(value)) {
4473
+ return err({
4474
+ code: "invalid_shape",
4475
+ path: "",
4476
+ message: "Legacy footer content must be an object."
4477
+ });
4478
+ }
4479
+ return validateFooterComposition({
4480
+ schemaVersion: 1,
4481
+ sections: defaultSections(value, options)
4482
+ });
4483
+ }
4484
+
4485
+ //#endregion
4486
+ //#region ../core/src/footer-navigation-layout.ts
4487
+ const footerNavigationLayouts = ["stack", "inline-wrap"];
4488
+ const defaultFooterNavigationLayout = "inline-wrap";
4489
+ function normalizeFooterNavigationLayout(value) {
4490
+ return value === "stack" ? "stack" : defaultFooterNavigationLayout;
4491
+ }
4492
+
4493
+ //#endregion
4494
+ //#region ../core/src/mailing-list-setup.ts
4495
+ function planMailingListFieldCompatibility(input) {
4496
+ const collectedFields = resolveCollectedGlobalFields(input.form);
4497
+ const requiredFields = resolveRequiredGlobalFields(input.form.fields);
4498
+ const issues = [];
4499
+ for (const destination of input.destinations) {
4500
+ for (const field of destination.requiredFields) {
4501
+ if (!collectedFields.includes(field)) {
4502
+ issues.push({
4503
+ tag: "destination_requires_uncollected_field",
4504
+ destinationKey: destination.destinationKey,
4505
+ field
4506
+ });
4507
+ } else if (!requiredFields.includes(field)) {
4508
+ issues.push({
4509
+ tag: "destination_requires_site_required_field",
4510
+ destinationKey: destination.destinationKey,
4511
+ field
4512
+ });
4513
+ }
4514
+ }
4515
+ for (const field of collectedFields) {
4516
+ if (!destination.acceptedFields.includes(field)) {
4517
+ issues.push({
4518
+ tag: requiredFields.includes(field) ? "destination_rejects_required_site_field" : "destination_rejects_collected_site_field",
4519
+ destinationKey: destination.destinationKey,
4520
+ field
4521
+ });
4522
+ }
4523
+ }
4524
+ }
4525
+ const [first, ...rest] = issues;
4526
+ return first === undefined ? { tag: "compatible" } : {
4527
+ tag: "blocked",
4528
+ issues: [first, ...rest]
4529
+ };
4530
+ }
4531
+ function resolveCollectedGlobalFields(form) {
4532
+ const phone = form.includeOptionalPhone ? ["phone"] : [];
4533
+ switch (form.fields.preset) {
4534
+ case "emailOnly": return phone;
4535
+ case "emailName": return ["name", ...phone];
4536
+ case "emailFirstLast": return [
4537
+ "first_name",
4538
+ "last_name",
4539
+ ...phone
4540
+ ];
4541
+ case "emailNameCompany": return [
4542
+ "name",
4543
+ "company",
4544
+ ...phone
4545
+ ];
4546
+ }
4547
+ }
4548
+ function resolveRequiredGlobalFields(fields) {
4549
+ switch (fields.preset) {
4550
+ case "emailOnly": return [];
4551
+ case "emailName": return fields.name === "required" ? ["name"] : [];
4552
+ case "emailFirstLast": return [...fields.firstName === "required" ? ["first_name"] : [], ...fields.lastName === "required" ? ["last_name"] : []];
4553
+ case "emailNameCompany": return [...fields.name === "required" ? ["name"] : [], ...fields.company === "required" ? ["company"] : []];
4554
+ }
4555
+ }
4556
+ function planMailingListSetupSwitch(input) {
4557
+ if (input.previousScope === null) {
4558
+ return {
4559
+ tag: "activate",
4560
+ affectedPlacementCount: 0
4561
+ };
4562
+ }
4563
+ if (sameProviderScope(input.previousScope, input.nextScope)) {
4564
+ return {
4565
+ tag: "update_same_scope",
4566
+ affectedPlacementCount: input.defaultChanged ? countDefaultMailingListPlacements(input.placementModes) : 0
4567
+ };
4568
+ }
4569
+ return {
4570
+ tag: "replace_scope",
4571
+ affectedPlacementCount: input.placementModes.length
4572
+ };
4573
+ }
4574
+ function sameProviderScope(previous, next) {
4575
+ if (previous.provider !== next.provider) return false;
4576
+ switch (previous.provider) {
4577
+ case "native": return true;
4578
+ case "mailchimp": return next.provider === "mailchimp" && previous.accountId === next.accountId;
4579
+ case "brevo": return next.provider === "brevo" && previous.accountId === next.accountId;
4580
+ }
4581
+ }
4582
+ function countDefaultMailingListPlacements(placementModes) {
4583
+ return placementModes.filter((mode) => mode === "default").length;
4584
+ }
4585
+
4586
+ //#endregion
4587
+ //#region ../core/src/mailing-list-setup-parser.ts
4588
+ function parseMailingListGlobalForm(value) {
4589
+ if (!isRecord$2(value)) return formError("shape");
4590
+ const fields = parseGlobalFields(value.fields);
4591
+ if (fields === null) return formError("fields");
4592
+ if (typeof value.includeOptionalPhone !== "boolean") {
4593
+ return formError("includeOptionalPhone");
4594
+ }
4595
+ const buttonLabel = parseRequiredString(value.buttonLabel);
4596
+ if (buttonLabel === null) return formError("buttonLabel");
4597
+ const privacyNote = parseOptionalString(value.privacyNote);
4598
+ if (privacyNote === undefined) return formError("privacyNote");
4599
+ return ok({
4600
+ fields,
4601
+ includeOptionalPhone: value.includeOptionalPhone,
4602
+ buttonLabel,
4603
+ privacyNote
4604
+ });
4605
+ }
4606
+ function parseMailingListSiteSetup(value) {
4607
+ if (!isRecord$2(value)) return setupError("shape");
4608
+ const siteId = parseSiteId(value.siteId);
4609
+ if (!siteId.ok) return setupError("siteId");
4610
+ if (value.tag === "not_configured") {
4611
+ return ok({
4612
+ tag: "not_configured",
4613
+ siteId: siteId.value
4614
+ });
4615
+ }
4616
+ if (value.tag !== "configured") return setupError("tag");
4617
+ const revision = parseMailingListSetupRevision(value.revision);
4618
+ if (revision === null) return setupError("revision");
4619
+ const form = parseMailingListGlobalForm(value.form);
4620
+ if (!form.ok) return setupError("form");
4621
+ const provider = parseConfiguredMailingListProvider(value.provider);
4622
+ if (!provider.ok) return provider;
4623
+ return ok({
4624
+ tag: "configured",
4625
+ siteId: siteId.value,
4626
+ revision,
4627
+ form: form.value,
4628
+ provider: provider.value
4629
+ });
4630
+ }
4631
+ function parseConfiguredMailingListProvider(value) {
4632
+ if (!isRecord$2(value) || typeof value.provider !== "string") {
4633
+ return setupError("provider");
4634
+ }
4635
+ switch (value.provider) {
4636
+ case "native": {
4637
+ const defaultTopicId = parseNewsletterListId(value.defaultTopicId);
4638
+ if (defaultTopicId === null) return setupError("defaultTargetId");
4639
+ const confirmation = parseToggleConfirmation(value.confirmation);
4640
+ return confirmation === null ? setupError("confirmation") : ok({
4641
+ provider: "native",
4642
+ defaultTopicId,
4643
+ confirmation
4644
+ });
4645
+ }
4646
+ case "mailchimp": {
4647
+ const connectionId = parseMailchimpConnectionId(value.connectionId);
4648
+ if (connectionId === null) return setupError("connectionId");
4649
+ const accountId = parseMailchimpAccountId(value.accountId);
4650
+ if (accountId === null) return setupError("accountId");
4651
+ const defaultAudienceId = parseMailchimpAudienceId(value.defaultAudienceId);
4652
+ if (defaultAudienceId === null) return setupError("defaultTargetId");
4653
+ const confirmation = parseToggleConfirmation(value.confirmation);
4654
+ return confirmation === null ? setupError("confirmation") : ok({
4655
+ provider: "mailchimp",
4656
+ connectionId,
4657
+ accountId,
4658
+ defaultAudienceId,
4659
+ confirmation
4660
+ });
4661
+ }
4662
+ case "brevo": {
4663
+ const connectionId = parseBrevoConnectionId(value.connectionId);
4664
+ if (connectionId === null) return setupError("connectionId");
4665
+ const accountId = parseBrevoAccountId(value.accountId);
4666
+ if (accountId === null) return setupError("accountId");
4667
+ const defaultListId = parseBrevoListId(value.defaultListId);
4668
+ if (defaultListId === null) return setupError("defaultTargetId");
4669
+ const confirmation = parseBrevoConfirmation(value.confirmation);
4670
+ return confirmation === null ? setupError("confirmation") : ok({
4671
+ provider: "brevo",
4672
+ connectionId,
4673
+ accountId,
4674
+ defaultListId,
4675
+ confirmation
4676
+ });
4677
+ }
4678
+ default: return setupError("provider");
4679
+ }
4680
+ }
4681
+ function parseGlobalFields(value) {
4682
+ if (!isRecord$2(value) || typeof value.preset !== "string") return null;
4683
+ switch (value.preset) {
4684
+ case "emailOnly": return { preset: "emailOnly" };
4685
+ case "emailName": return isRequirement(value.name) ? {
4686
+ preset: "emailName",
4687
+ name: value.name
4688
+ } : null;
4689
+ case "emailFirstLast": return isRequirement(value.firstName) && isRequirement(value.lastName) ? {
4690
+ preset: "emailFirstLast",
4691
+ firstName: value.firstName,
4692
+ lastName: value.lastName
4693
+ } : null;
4694
+ case "emailNameCompany": return isRequirement(value.name) && isRequirement(value.company) ? {
4695
+ preset: "emailNameCompany",
4696
+ name: value.name,
4697
+ company: value.company
4698
+ } : null;
4699
+ default: return null;
4700
+ }
4701
+ }
4702
+ function parseToggleConfirmation(value) {
4703
+ if (!isRecord$2(value)) return null;
4704
+ return value.tag === "on" || value.tag === "off" ? { tag: value.tag } : null;
4705
+ }
4706
+ function parseBrevoConfirmation(value) {
4707
+ if (!isRecord$2(value)) return null;
4708
+ if (value.tag === "off") return { tag: "off" };
4709
+ if (value.tag !== "on") return null;
4710
+ const templateId = parseBrevoConfirmationTemplateId(value.templateId);
4711
+ const redirectUrl = parseHttpUrl(value.redirectUrl);
4712
+ return templateId === null || redirectUrl === null ? null : {
4713
+ tag: "on",
4714
+ templateId,
4715
+ redirectUrl
4716
+ };
4717
+ }
4718
+ function parseMailingListSetupRevision(value) {
4719
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
4720
+ }
4721
+ function parseHttpUrl(value) {
4722
+ if (typeof value !== "string") return null;
4723
+ try {
4724
+ const url = new URL(value);
4725
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null;
4726
+ } catch {
4727
+ return null;
4728
+ }
4729
+ }
4730
+ function parseRequiredString(value) {
4731
+ if (typeof value !== "string") return null;
4732
+ const trimmed = value.trim();
4733
+ return trimmed.length > 0 ? trimmed : null;
4734
+ }
4735
+ function parseOptionalString(value) {
4736
+ if (value === null || value === undefined) return null;
4737
+ if (typeof value !== "string") return undefined;
4738
+ const trimmed = value.trim();
4739
+ return trimmed.length > 0 ? trimmed : null;
4740
+ }
4741
+ function isRequirement(value) {
4742
+ return value === "required" || value === "optional";
4743
+ }
4744
+ function isRecord$2(value) {
4745
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4746
+ }
4747
+ function formError(field) {
4748
+ return err({
4749
+ tag: "invalid_mailing_list_global_form",
4750
+ field
4751
+ });
4752
+ }
4753
+ function setupError(field) {
4754
+ return err({
4755
+ tag: "invalid_mailing_list_setup",
4756
+ field
4757
+ });
4758
+ }
4759
+
4760
+ //#endregion
4761
+ //#region ../core/src/mailing-list-targeting.ts
4762
+ function sameMailingListPlacementTargeting(left, right) {
4763
+ if (left.mode !== right.mode) return false;
4764
+ if (left.mode === "default" || right.mode === "default") return true;
4765
+ if (left.provider !== right.provider || left.selection.tag !== right.selection.tag) return false;
4766
+ if (left.provider === "mailchimp" && (right.provider !== "mailchimp" || left.audienceId !== right.audienceId)) return false;
4767
+ return sameIds(targetingSelectionIds(left), targetingSelectionIds(right));
4768
+ }
4769
+ function targetingSelectionIds(targeting) {
4770
+ switch (targeting.provider) {
4771
+ case "native": return targeting.selection.tag === "fixed" ? targeting.selection.topicIds : targeting.selection.offeredTopicIds;
4772
+ case "brevo": return targeting.selection.tag === "fixed" ? targeting.selection.listIds : targeting.selection.offeredListIds;
4773
+ case "mailchimp": return targeting.selection.tag === "fixed" ? targeting.selection.interestIds : targeting.selection.offeredInterestIds;
4774
+ }
4775
+ }
4776
+ function sameIds(left, right) {
4777
+ const leftIds = new Set(left);
4778
+ const rightIds = new Set(right);
4779
+ return leftIds.size === left.length && rightIds.size === right.length && leftIds.size === rightIds.size && [...leftIds].every((id) => rightIds.has(id));
4780
+ }
4781
+ function resolveMailingListTarget(input) {
4782
+ if (input.targeting.mode === "default") {
4783
+ if (input.visitorSelection !== undefined) {
4784
+ return err({ tag: "visitor_selection_not_allowed" });
4785
+ }
4786
+ return ok(resolveDefaultTarget(input.setup));
4787
+ }
4788
+ if (input.targeting.provider !== input.setup.provider) {
4789
+ return err({
4790
+ tag: "targeting_provider_mismatch",
4791
+ setupProvider: input.setup.provider,
4792
+ targetingProvider: input.targeting.provider
4793
+ });
4794
+ }
4795
+ return resolveCustomTarget(input.targeting, input.visitorSelection);
4796
+ }
4797
+ function resolveDefaultTarget(setup) {
4798
+ switch (setup.provider) {
4799
+ case "native": return {
4800
+ provider: "native",
4801
+ topicIds: [setup.defaultTopicId]
4802
+ };
4803
+ case "brevo": return {
4804
+ provider: "brevo",
4805
+ listIds: [setup.defaultListId]
4806
+ };
4807
+ case "mailchimp": return {
4808
+ provider: "mailchimp",
4809
+ audienceId: setup.defaultAudienceId,
4810
+ interestIds: []
4811
+ };
4812
+ }
4813
+ }
4814
+ function resolveCustomTarget(targeting, visitorSelection) {
4815
+ switch (targeting.provider) {
4816
+ case "native": return resolveNativeTarget(targeting, visitorSelection);
4817
+ case "brevo": return resolveBrevoTarget(targeting, visitorSelection);
4818
+ case "mailchimp": return resolveMailchimpTarget(targeting, visitorSelection);
4819
+ }
4820
+ }
4821
+ function resolveNativeTarget(targeting, selection) {
4822
+ if (targeting.selection.tag === "fixed") {
4823
+ return selection === undefined ? ok({
4824
+ provider: "native",
4825
+ topicIds: targeting.selection.topicIds
4826
+ }) : err({ tag: "visitor_selection_not_allowed" });
4827
+ }
4828
+ if (selection === undefined) {
4829
+ return targeting.selection.offeredTopicIds.length === 1 ? ok({
4830
+ provider: "native",
4831
+ topicIds: targeting.selection.offeredTopicIds
4832
+ }) : err({ tag: "visitor_selection_required" });
4833
+ }
4834
+ if (selection.provider !== "native") {
4835
+ return err({ tag: "visitor_selection_provider_mismatch" });
4836
+ }
4837
+ const selected = selectedNonEmptySubset(selection.topicIds, targeting.selection.offeredTopicIds);
4838
+ return selected.ok ? ok({
4839
+ provider: "native",
4840
+ topicIds: selected.value
4841
+ }) : err({ tag: "visitor_selection_outside_offer" });
4842
+ }
4843
+ function resolveBrevoTarget(targeting, selection) {
4844
+ if (targeting.selection.tag === "fixed") {
4845
+ return selection === undefined ? ok({
4846
+ provider: "brevo",
4847
+ listIds: targeting.selection.listIds
4848
+ }) : err({ tag: "visitor_selection_not_allowed" });
4849
+ }
4850
+ if (selection === undefined) {
4851
+ return targeting.selection.offeredListIds.length === 1 ? ok({
4852
+ provider: "brevo",
4853
+ listIds: targeting.selection.offeredListIds
4854
+ }) : err({ tag: "visitor_selection_required" });
4855
+ }
4856
+ if (selection.provider !== "brevo") {
4857
+ return err({ tag: "visitor_selection_provider_mismatch" });
4858
+ }
4859
+ const selected = selectedNonEmptySubset(selection.listIds, targeting.selection.offeredListIds);
4860
+ return selected.ok ? ok({
4861
+ provider: "brevo",
4862
+ listIds: selected.value
4863
+ }) : err({ tag: "visitor_selection_outside_offer" });
4864
+ }
4865
+ function resolveMailchimpTarget(targeting, selection) {
4866
+ if (targeting.selection.tag === "fixed") {
4867
+ return selection === undefined ? ok({
4868
+ provider: "mailchimp",
4869
+ audienceId: targeting.audienceId,
4870
+ interestIds: targeting.selection.interestIds
4871
+ }) : err({ tag: "visitor_selection_not_allowed" });
4872
+ }
4873
+ if (selection === undefined) {
4874
+ return targeting.selection.offeredInterestIds.length === 1 ? ok({
4875
+ provider: "mailchimp",
4876
+ audienceId: targeting.audienceId,
4877
+ interestIds: targeting.selection.offeredInterestIds
4878
+ }) : err({ tag: "visitor_selection_required" });
4879
+ }
4880
+ if (selection.provider !== "mailchimp") {
4881
+ return err({ tag: "visitor_selection_provider_mismatch" });
4882
+ }
4883
+ const subset = selectedNonEmptySubset(selection.interestIds, targeting.selection.offeredInterestIds);
4884
+ return subset.ok ? ok({
4885
+ provider: "mailchimp",
4886
+ audienceId: targeting.audienceId,
4887
+ interestIds: subset.value
4888
+ }) : err({ tag: "visitor_selection_outside_offer" });
4889
+ }
4890
+ function selectedNonEmptySubset(selected, offered) {
4891
+ const normalized = [...new Set(selected)];
4892
+ const [first, ...rest] = normalized;
4893
+ if (first === undefined || normalized.some((item) => !offered.includes(item))) {
4894
+ return err("invalid_selection");
4895
+ }
4896
+ return ok([first, ...rest]);
4897
+ }
4898
+ function planMailingListChoicePresentation(targeting) {
4899
+ if (targeting.mode === "default" || targeting.selection.tag === "fixed") {
4900
+ return { tag: "hidden" };
4901
+ }
4902
+ switch (targeting.provider) {
4903
+ case "native": return targeting.selection.offeredTopicIds.length === 1 ? {
4904
+ tag: "automatic",
4905
+ selection: {
4906
+ provider: "native",
4907
+ topicIds: targeting.selection.offeredTopicIds
4908
+ }
4909
+ } : {
4910
+ tag: "visitor_choice",
4911
+ provider: "native",
4912
+ optionCount: targeting.selection.offeredTopicIds.length
4913
+ };
4914
+ case "brevo": return targeting.selection.offeredListIds.length === 1 ? {
4915
+ tag: "automatic",
4916
+ selection: {
4917
+ provider: "brevo",
4918
+ listIds: targeting.selection.offeredListIds
4919
+ }
4920
+ } : {
4921
+ tag: "visitor_choice",
4922
+ provider: "brevo",
4923
+ optionCount: targeting.selection.offeredListIds.length
4924
+ };
4925
+ case "mailchimp": return targeting.selection.offeredInterestIds.length === 1 ? {
4926
+ tag: "automatic",
4927
+ selection: {
4928
+ provider: "mailchimp",
4929
+ interestIds: targeting.selection.offeredInterestIds
4930
+ }
4931
+ } : {
4932
+ tag: "visitor_choice",
4933
+ provider: "mailchimp",
4934
+ optionCount: targeting.selection.offeredInterestIds.length
4935
+ };
4936
+ }
4937
+ }
4938
+
4939
+ //#endregion
4940
+ //#region ../core/src/mailing-list-runtime.ts
4941
+ function parseMailingListRuntimeContext(value) {
4942
+ if (!isRecord$1(value)) return invalidRuntimeContext();
4943
+ if (value.tag === "not_configured") return ok({ tag: "not_configured" });
4944
+ if (value.tag !== "configured") return invalidRuntimeContext();
4945
+ const revision = parseMailingListSetupRevision(value.revision);
4946
+ const form = parseMailingListGlobalForm(value.form);
4947
+ const provider = parseRuntimeProvider(value.provider);
4948
+ if (revision === null || !form.ok || provider === null) {
4949
+ return invalidRuntimeContext();
3734
4950
  }
4951
+ return ok({
4952
+ tag: "configured",
4953
+ revision,
4954
+ form: form.value,
4955
+ provider
4956
+ });
3735
4957
  }
3736
- function deriveFooterEditorOverview(composition) {
4958
+ function parseRuntimeProvider(value) {
4959
+ if (!isRecord$1(value)) return null;
4960
+ switch (value.provider) {
4961
+ case "native": return parseNativeRuntimeProvider(value);
4962
+ case "brevo": return parseBrevoRuntimeProvider(value);
4963
+ case "mailchimp": return parseMailchimpRuntimeProvider(value);
4964
+ default: return null;
4965
+ }
4966
+ }
4967
+ function parseNativeRuntimeProvider(value) {
4968
+ const defaultTopicId = parseNewsletterListId(value.defaultTopicId);
4969
+ const topics = parseNamedItems(value.topics, parseNewsletterListId);
4970
+ return defaultTopicId !== null && topics !== null ? {
4971
+ provider: "native",
4972
+ defaultTopicId,
4973
+ topics
4974
+ } : null;
4975
+ }
4976
+ function parseBrevoRuntimeProvider(value) {
4977
+ const defaultListId = parseBrevoListId(value.defaultListId);
4978
+ const lists = parseNamedItems(value.lists, parseBrevoListId);
4979
+ return defaultListId !== null && lists !== null ? {
4980
+ provider: "brevo",
4981
+ defaultListId,
4982
+ lists
4983
+ } : null;
4984
+ }
4985
+ function parseMailchimpRuntimeProvider(value) {
4986
+ if (!Array.isArray(value.audiences)) return null;
4987
+ const defaultAudienceId = parseMailchimpAudienceId(value.defaultAudienceId);
4988
+ if (defaultAudienceId === null) return null;
4989
+ const audiences = [];
4990
+ for (const audience of value.audiences) {
4991
+ if (!isRecord$1(audience)) return null;
4992
+ const id = parseMailchimpAudienceId(audience.id);
4993
+ const name = parseName(audience.name);
4994
+ if (id === null || name === null || !Array.isArray(audience.interests)) {
4995
+ return null;
4996
+ }
4997
+ const interests = parseMailchimpInterests(audience.interests);
4998
+ if (interests === null) return null;
4999
+ audiences.push({
5000
+ id,
5001
+ name,
5002
+ interests
5003
+ });
5004
+ }
3737
5005
  return {
3738
- selected: composition.sections.filter((section) => section.selected).map((section) => ({
3739
- section,
3740
- status: "selected"
3741
- })),
3742
- available: composition.sections.filter((section) => !section.selected).map((section) => ({
3743
- section,
3744
- status: "available"
3745
- }))
5006
+ provider: "mailchimp",
5007
+ defaultAudienceId,
5008
+ audiences
3746
5009
  };
3747
5010
  }
3748
- const defaultSections = (legacy, options) => {
3749
- const footerVisible = options.footerVisible ?? true;
3750
- return [
3751
- {
3752
- id: unsafeAsFooterModuleId("brand"),
3753
- kind: "brand",
3754
- heading: null,
3755
- selected: footerVisible,
3756
- showLogo: options.brand?.showLogo ?? true,
3757
- showTitle: options.brand?.showTitle ?? true
3758
- },
3759
- {
3760
- id: unsafeAsFooterModuleId("navigation"),
3761
- kind: "navigation",
3762
- heading: null,
3763
- selected: footerVisible,
3764
- source: {
3765
- kind: "menu-role",
3766
- role: "main"
3767
- }
3768
- },
3769
- {
3770
- id: unsafeAsFooterModuleId("legal-navigation"),
3771
- kind: "legal-navigation",
3772
- heading: null,
3773
- selected: false,
3774
- source: {
3775
- kind: "menu-role",
3776
- role: "legal-policy"
3777
- }
3778
- },
3779
- {
3780
- id: unsafeAsFooterModuleId("business-address"),
3781
- kind: "business-address",
3782
- heading: null,
3783
- selected: footerVisible && legacy.showBusinessAddress === true,
3784
- source: { kind: "business-address" }
3785
- },
3786
- {
3787
- id: unsafeAsFooterModuleId("contact"),
3788
- kind: "contact",
3789
- heading: null,
3790
- selected: false,
3791
- source: { kind: "public-profile" }
3792
- },
3793
- {
3794
- id: unsafeAsFooterModuleId("opening-hours"),
3795
- kind: "opening-hours",
3796
- heading: null,
3797
- selected: false,
3798
- source: { kind: "public-profile" }
3799
- },
3800
- {
3801
- id: unsafeAsFooterModuleId("social-links"),
3802
- kind: "social-links",
3803
- heading: null,
3804
- selected: false,
3805
- source: { kind: "public-profile" }
3806
- },
3807
- {
3808
- id: unsafeAsFooterModuleId("supporting-text"),
3809
- kind: "supporting-text",
3810
- heading: null,
3811
- selected: footerVisible && isRichText(legacy.bodyText),
3812
- content: isRichText(legacy.bodyText) ? legacy.bodyText : null
3813
- },
3814
- {
3815
- id: unsafeAsFooterModuleId("legal"),
3816
- kind: "legal",
3817
- heading: null,
3818
- selected: footerVisible && isRichText(legacy.bottomText),
3819
- content: isRichText(legacy.bottomText) ? legacy.bottomText : null
3820
- }
3821
- ];
3822
- };
3823
- function normalizeLegacyFooterContentToV1(value, options = {}) {
3824
- if (!isRecord$1(value)) {
3825
- return err({
3826
- code: "invalid_shape",
3827
- path: "",
3828
- message: "Legacy footer content must be an object."
5011
+ function parseMailchimpInterests(value) {
5012
+ const interests = [];
5013
+ for (const interest of value) {
5014
+ if (!isRecord$1(interest)) return null;
5015
+ const id = parseMailchimpInterestId(interest.id);
5016
+ const name = parseName(interest.name);
5017
+ const categoryName = parseName(interest.categoryName);
5018
+ if (id === null || name === null || categoryName === null) return null;
5019
+ interests.push({
5020
+ id,
5021
+ name,
5022
+ categoryName
3829
5023
  });
3830
5024
  }
3831
- return validateFooterComposition({
3832
- schemaVersion: 1,
3833
- sections: defaultSections(value, options)
3834
- });
5025
+ return interests;
5026
+ }
5027
+ function parseNamedItems(value, parseId) {
5028
+ if (!Array.isArray(value)) return null;
5029
+ const items = [];
5030
+ for (const item of value) {
5031
+ if (!isRecord$1(item)) return null;
5032
+ const id = parseId(item.id);
5033
+ const name = parseName(item.name);
5034
+ if (id === null || name === null) return null;
5035
+ items.push({
5036
+ id,
5037
+ name
5038
+ });
5039
+ }
5040
+ return items;
5041
+ }
5042
+ function parseName(value) {
5043
+ if (typeof value !== "string") return null;
5044
+ const name = value.trim();
5045
+ return name.length > 0 ? name : null;
5046
+ }
5047
+ function isRecord$1(value) {
5048
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5049
+ }
5050
+ function invalidRuntimeContext() {
5051
+ return err({ tag: "invalid_mailing_list_runtime_context" });
3835
5052
  }
3836
5053
 
3837
5054
  //#endregion
3838
- //#region ../core/src/footer-navigation-layout.ts
3839
- const footerNavigationLayouts = ["stack", "inline-wrap"];
3840
- const defaultFooterNavigationLayout = "inline-wrap";
3841
- function normalizeFooterNavigationLayout(value) {
3842
- return value === "stack" ? "stack" : defaultFooterNavigationLayout;
5055
+ //#region ../core/src/mailing-list-submission.ts
5056
+ const MAILING_LIST_SUBMISSION_BUDGET_MS = 1e4;
5057
+ function parseMailingListBlockId(value) {
5058
+ const parsed = typeof value === "string" ? value.trim() : "";
5059
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(parsed) ? parsed : null;
5060
+ }
5061
+ function parseMailingListSignedSourceToken(value) {
5062
+ const parsed = typeof value === "string" ? value.trim() : "";
5063
+ return /^[A-Za-z0-9._~-]{16,1000}$/u.test(parsed) ? parsed : null;
5064
+ }
5065
+ function parseMailingListSdkFooterBlockId(value) {
5066
+ const parsed = typeof value === "string" ? value.trim() : "";
5067
+ return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(parsed) ? parsed : null;
5068
+ }
5069
+ function parseMailingListSdkContentHash(value) {
5070
+ const parsed = typeof value === "string" ? value.trim() : "";
5071
+ return /^[a-f0-9]{16}$/iu.test(parsed) ? parsed : null;
5072
+ }
5073
+ function parseMailingListContentVersionId(value) {
5074
+ const parsed = parseContentVersionId(value);
5075
+ return parsed.ok ? parsed.value : null;
5076
+ }
5077
+ function parseMailingListFooterRevision(value) {
5078
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
5079
+ }
5080
+ function buildMailingListSignupBlockSourceRef(blockId) {
5081
+ const parsed = parseMailingListBlockId(blockId);
5082
+ return parsed !== null ? {
5083
+ tag: "block_ref",
5084
+ blockId: parsed
5085
+ } : null;
5086
+ }
5087
+ function parseMailingListSignupProfile(form, input) {
5088
+ const email = normalizeSubscriberEmail(input.email);
5089
+ if (email === null) return {
5090
+ ok: false,
5091
+ error: { tag: "invalid_email" }
5092
+ };
5093
+ const phone = form.includeOptionalPhone ? {
5094
+ tag: "collected",
5095
+ value: normalizeOptionalValue(input.phone)
5096
+ } : { tag: "not_collected" };
5097
+ switch (form.fields.preset) {
5098
+ case "emailOnly": return {
5099
+ ok: true,
5100
+ value: {
5101
+ preset: "emailOnly",
5102
+ email,
5103
+ phone
5104
+ }
5105
+ };
5106
+ case "emailName": {
5107
+ const name = resolveProfileField("name", form.fields.name, input.name);
5108
+ return name.ok ? {
5109
+ ok: true,
5110
+ value: {
5111
+ preset: "emailName",
5112
+ email,
5113
+ phone,
5114
+ name: name.value
5115
+ }
5116
+ } : name;
5117
+ }
5118
+ case "emailFirstLast": {
5119
+ const firstName = resolveProfileField("first_name", form.fields.firstName, input.firstName);
5120
+ if (!firstName.ok) return firstName;
5121
+ const lastName = resolveProfileField("last_name", form.fields.lastName, input.lastName);
5122
+ return lastName.ok ? {
5123
+ ok: true,
5124
+ value: {
5125
+ preset: "emailFirstLast",
5126
+ email,
5127
+ phone,
5128
+ firstName: firstName.value,
5129
+ lastName: lastName.value
5130
+ }
5131
+ } : lastName;
5132
+ }
5133
+ case "emailNameCompany": {
5134
+ const name = resolveProfileField("name", form.fields.name, input.name);
5135
+ if (!name.ok) return name;
5136
+ const company = resolveProfileField("company", form.fields.company, input.company);
5137
+ return company.ok ? {
5138
+ ok: true,
5139
+ value: {
5140
+ preset: "emailNameCompany",
5141
+ email,
5142
+ phone,
5143
+ name: name.value,
5144
+ company: company.value
5145
+ }
5146
+ } : company;
5147
+ }
5148
+ }
5149
+ }
5150
+ function resolveProfileField(field, requirement, input) {
5151
+ const value = normalizeOptionalValue(input);
5152
+ if (requirement === "required") {
5153
+ return value === null ? {
5154
+ ok: false,
5155
+ error: {
5156
+ tag: "required_field_missing",
5157
+ field
5158
+ }
5159
+ } : {
5160
+ ok: true,
5161
+ value: {
5162
+ requirement: "required",
5163
+ value
5164
+ }
5165
+ };
5166
+ }
5167
+ return {
5168
+ ok: true,
5169
+ value: {
5170
+ requirement: "optional",
5171
+ value
5172
+ }
5173
+ };
5174
+ }
5175
+ function normalizeSubscriberEmail(value) {
5176
+ const normalized = value.trim().toLowerCase();
5177
+ const at = normalized.indexOf("@");
5178
+ return normalized.length <= 320 && at > 0 && at === normalized.lastIndexOf("@") && at < normalized.length - 1 ? normalized : null;
5179
+ }
5180
+ function normalizeOptionalValue(value) {
5181
+ const normalized = value?.trim() ?? "";
5182
+ return normalized.length > 0 ? normalized : null;
5183
+ }
5184
+ function toMailingListPublicSignupResponse(result) {
5185
+ if (result.ok) {
5186
+ return result.acknowledgement.tag === "subscribed" ? { tag: "subscribed" } : { tag: "confirmation_required" };
5187
+ }
5188
+ if (result.error.tag === "profile_rejected") {
5189
+ return {
5190
+ tag: "invalid_input",
5191
+ reason: "profile_rejected"
5192
+ };
5193
+ }
5194
+ return result.error.tag === "uncertain" ? {
5195
+ tag: "uncertain",
5196
+ reason: result.error.reason
5197
+ } : {
5198
+ tag: "unavailable",
5199
+ reason: result.error.tag
5200
+ };
3843
5201
  }
3844
5202
 
3845
5203
  //#endregion
@@ -4653,61 +6011,161 @@ function foldSiteRouteTarget(target, cases) {
4653
6011
  }
4654
6012
 
4655
6013
  //#endregion
4656
- //#region ../core/src/planner.ts
4657
- function plannerNoChange(details = {}) {
4658
- return {
4659
- ...details,
4660
- tag: "no_change"
4661
- };
6014
+ //#region ../core/src/whole-series-run-schedule.ts
6015
+ /**
6016
+ * A session that is not happening.
6017
+ *
6018
+ * Exported because every surface that renders a run's schedule has to mark the
6019
+ * same weeks, and a second copy of the string is a second chance to disagree.
6020
+ */
6021
+ const CANCELLED_SESSION_STATUS = "cancelled";
6022
+ /** A run the organiser called off. Spelled the same, about a different row. */
6023
+ const CANCELLED_SERIES_STATUS = "cancelled";
6024
+ function sessionStartMs(session) {
6025
+ return Date.parse(session.startsAt);
4662
6026
  }
4663
- function plannerUpdate(details = {}) {
4664
- return {
4665
- ...details,
4666
- tag: "update"
4667
- };
6027
+ /**
6028
+ * The run's sessions that still count, in schedule order.
6029
+ *
6030
+ * Sessions whose start cannot be read are dropped rather than sorted to an
6031
+ * arbitrary end: an unparseable date is not a date a visitor can attend.
6032
+ */
6033
+ function orderedLiveRunSessions(sessions) {
6034
+ return sessions.filter((session) => session.status !== CANCELLED_SESSION_STATUS && Number.isFinite(sessionStartMs(session))).sort((left, right) => sessionStartMs(left) - sessionStartMs(right));
4668
6035
  }
4669
- /** Identity at runtime; marks a planner-derived patch as shallow readonly. */
4670
- function plannerPatch(patch) {
4671
- return patch;
6036
+ /** When the first session a visitor could attend begins. */
6037
+ function firstLiveRunSessionStartMs(sessions) {
6038
+ const first = orderedLiveRunSessions(sessions)[0];
6039
+ return first === undefined ? null : sessionStartMs(first);
4672
6040
  }
4673
- /** Default clock for production callers that do not inject time explicitly. */
4674
- const systemPlanningClock = { now: () => Date.now() };
4675
- /** Resolve current time from an explicit timestamp, injected clock, or system clock. */
4676
- function resolvePlanningNow(input = {}) {
4677
- if (typeof input.now === "number") {
4678
- return input.now;
4679
- }
4680
- return (input.clock ?? systemPlanningClock).now();
6041
+ /**
6042
+ * Booking stays open until the run's first surviving session has started.
6043
+ *
6044
+ * There has to be something left to sell: a run with no schedule at all, or one
6045
+ * whose every session has been cancelled, is closed rather than open. Adding a
6046
+ * future session opens it again, subject to the usual status, visibility and
6047
+ * admission rules.
6048
+ */
6049
+ function isWholeSeriesRegistrationOpen(sessions, now) {
6050
+ const firstStart = firstLiveRunSessionStartMs(sessions);
6051
+ return firstStart !== null && firstStart > now.getTime();
4681
6052
  }
4682
- /** Resolve current time as a Date from an explicit Date, injected clock, or system clock. */
4683
- function resolvePlanningNowDate(input = {}) {
4684
- if (input.now !== undefined) {
4685
- return input.now;
4686
- }
4687
- return new Date((input.clock ?? systemPlanningClock).now());
6053
+ /**
6054
+ * Where the run sits in its own life, independent of any one session.
6055
+ *
6056
+ * A cancelled or completed series status says so whatever the dates read.
6057
+ * Otherwise the surviving schedule decides: before the first session it is
6058
+ * upcoming, after the last session ends it is complete, and in between it is
6059
+ * under way. A run with nothing left to attend is `upcoming` and never
6060
+ * `in_progress` — an empty schedule has not started.
6061
+ */
6062
+ function resolveWholeSeriesRunLifecycle(input, now) {
6063
+ if (input.seriesStatus === CANCELLED_SERIES_STATUS) return "cancelled";
6064
+ if (input.seriesStatus === "completed") return "completed";
6065
+ const live = orderedLiveRunSessions(input.sessions);
6066
+ const first = live[0];
6067
+ const last = live[live.length - 1];
6068
+ if (first === undefined || last === undefined) return "upcoming";
6069
+ if (sessionStartMs(first) > now.getTime()) return "upcoming";
6070
+ return Date.parse(last.endsAt) < now.getTime() ? "completed" : "in_progress";
4688
6071
  }
4689
6072
  /**
4690
- * Resolve a Date-compatible planner input.
6073
+ * Whether a prepared run is still inviting bookings at render time.
4691
6074
  *
4692
- * Precedence: bare Date, `{ now }`, `{ clock }`, then `systemPlanningClock`.
4693
- * Explicit Date inputs are returned by reference, not cloned.
6075
+ * `lifecycle` and `enrollmentOpen` come from the projection; the first session
6076
+ * start is the one fact re-read against `now`, because that is the boundary a
6077
+ * cached page can cross while it sits in front of a visitor.
4694
6078
  */
4695
- function resolvePlanningDateInput(input = {}) {
4696
- if (input instanceof Date) {
4697
- return input;
6079
+ function resolveWholeSeriesBookingPhase(run, now) {
6080
+ switch (run.lifecycle) {
6081
+ case "cancelled": return {
6082
+ kind: "closed",
6083
+ reason: "cancelled"
6084
+ };
6085
+ case "in_progress": return {
6086
+ kind: "closed",
6087
+ reason: "in-progress"
6088
+ };
6089
+ case "completed": return {
6090
+ kind: "closed",
6091
+ reason: "completed"
6092
+ };
6093
+ case "upcoming": {
6094
+ if (firstLiveRunSessionStartMs(run.sessions) === null) {
6095
+ return {
6096
+ kind: "closed",
6097
+ reason: "no-sessions"
6098
+ };
6099
+ }
6100
+ if (!isWholeSeriesRegistrationOpen(run.sessions, now)) {
6101
+ return {
6102
+ kind: "closed",
6103
+ reason: "in-progress"
6104
+ };
6105
+ }
6106
+ return run.enrollmentOpen ? { kind: "open" } : {
6107
+ kind: "closed",
6108
+ reason: "enrolment-closed"
6109
+ };
6110
+ }
4698
6111
  }
4699
- return resolvePlanningNowDate(input);
4700
6112
  }
4701
- /** Build an enabled selection while preserving literal value types. */
4702
- function enabledSelection(value) {
6113
+
6114
+ //#endregion
6115
+ //#region ../core/src/public-read-cache-contract.ts
6116
+ const PUBLIC_AVAILABILITY_FRESHNESS_SECONDS = 60;
6117
+ const PUBLIC_CONTENT_FRESHNESS_SECONDS = 3600;
6118
+ function availability(transport) {
4703
6119
  return {
4704
- enabled: true,
4705
- value
6120
+ kind: "live",
6121
+ reason: "booking_correctness",
6122
+ freshness: "availability",
6123
+ transport
4706
6124
  };
4707
6125
  }
4708
- /** Build a disabled selection. */
4709
- function disabledSelection() {
4710
- return { enabled: false };
6126
+ function content(tags, transport = "direct") {
6127
+ return {
6128
+ kind: "tagged",
6129
+ tags,
6130
+ freshness: "content",
6131
+ transport
6132
+ };
6133
+ }
6134
+ /** The single classification authority for cacheable public reads. */
6135
+ const PUBLIC_READ_CACHE_POLICIES = {
6136
+ listPublishedEntries: content(["published_content"]),
6137
+ getPublishedEntryPreview: content(["published_content"]),
6138
+ listPublicEvents: availability("direct"),
6139
+ listPublicEventCalendarWindow: availability("direct"),
6140
+ listPublicRecurringSchedule: availability("batch_only"),
6141
+ getPublicEventDetails: availability("batch_only"),
6142
+ resolveEventSubroute: availability("direct"),
6143
+ listPublicRouteCandidates: content(["published_content"], "batch_only"),
6144
+ listPublicTeamMembers: content(["public_team_members"]),
6145
+ listPublicProducts: availability("direct"),
6146
+ listPublicPasses: availability("direct"),
6147
+ listPublicMemberships: availability("direct"),
6148
+ getPublicFormById: content(["site_forms"]),
6149
+ getPublicBookingServices: availability("direct"),
6150
+ getPublicBookingService: availability("direct"),
6151
+ getPublicBookingResourcesReference: availability("direct"),
6152
+ getPublicBookingAvailableDates: availability("direct"),
6153
+ getPublicBookingAvailableSlots: availability("direct"),
6154
+ getPublicWeeklySlotAvailability: availability("direct"),
6155
+ listPublicForms: content(["site_forms"]),
6156
+ checkRedirect: content([]),
6157
+ getPublicRoutableContent: content(["published_content"]),
6158
+ getPublicSitemapRoutes: content(["published_content"])
6159
+ };
6160
+ const PUBLIC_READ_ENDPOINT_SET = new Set(Object.keys(PUBLIC_READ_CACHE_POLICIES));
6161
+ function isPublicReadEndpoint(endpoint) {
6162
+ return PUBLIC_READ_ENDPOINT_SET.has(endpoint);
6163
+ }
6164
+ function getPublicReadCachePolicy(endpoint) {
6165
+ return PUBLIC_READ_CACHE_POLICIES[endpoint];
6166
+ }
6167
+ function publicReadFreshnessSeconds(endpoint) {
6168
+ return getPublicReadCachePolicy(endpoint).freshness === "availability" ? PUBLIC_AVAILABILITY_FRESHNESS_SECONDS : PUBLIC_CONTENT_FRESHNESS_SECONDS;
4711
6169
  }
4712
6170
 
4713
6171
  //#endregion
@@ -4793,13 +6251,13 @@ const BOOKING_DEFAULTS_BY_BEHAVIOUR = {
4793
6251
  memberships: "off"
4794
6252
  }
4795
6253
  };
4796
- function verticalBookingPreset(id, verticalId, canonicalBehaviour, label, pluralLabel, description, availability = "onboarding") {
6254
+ function verticalBookingPreset(id, verticalId, canonicalBehaviour, label, pluralLabel, description, availability$1 = "onboarding") {
4797
6255
  return {
4798
6256
  id,
4799
6257
  verticalId,
4800
6258
  canonicalBehaviour,
4801
6259
  engine: BOOKING_ENGINE_BY_BEHAVIOUR[canonicalBehaviour],
4802
- availability,
6260
+ availability: availability$1,
4803
6261
  label,
4804
6262
  pluralLabel,
4805
6263
  description
@@ -5153,12 +6611,12 @@ const VERTICAL_PRESET_AVAILABILITIES = [
5153
6611
  "future"
5154
6612
  ];
5155
6613
  function getVerticalPresetsForVertical(verticalId, options) {
5156
- const availability = options?.availability;
6614
+ const availability$1 = options?.availability;
5157
6615
  return VERTICAL_BOOKING_PRESETS.filter((presetDefinition) => {
5158
6616
  if (presetDefinition.verticalId !== verticalId) return false;
5159
- if (!availability) return true;
5160
- if (availability === "selectable") return presetDefinition.availability !== "future";
5161
- return presetDefinition.availability === availability;
6617
+ if (!availability$1) return true;
6618
+ if (availability$1 === "selectable") return presetDefinition.availability !== "future";
6619
+ return presetDefinition.availability === availability$1;
5162
6620
  });
5163
6621
  }
5164
6622
  function getOnboardingPresetsForVertical(verticalId) {
@@ -5382,4 +6840,62 @@ function orderPresetIdsForVertical(verticalId, presetIds) {
5382
6840
  }
5383
6841
 
5384
6842
  //#endregion
5385
- export { parseEventSeriesId as $, normalizeLocationAddressText as A, ok as At, parseUtcIsoDateTimeString as B, parseNewsletterSignupDestinationProvider as C, unsafeAsSiteScopedStoragePath as Ct, mapNonEmptyArray as D, unsafeAsUploadSessionId as Dt, fromArray as E, unsafeAsStoragePath as Et, formatLocalScheduleDate as F, parseEventPageSurfaceSubject as G, toScheduledBookingDateRange as H, formatLocalScheduleTime as I, publicEventPricingCurrency as J, formatBookingPriceDisplay as K, getBookingLocalDateKey as L, formatBookingDate as M, formatBookingDateParts as N, hasText as O, unsafeAsUserId as Ot, formatBookingTime as P, parseApiKeyId as Q, getLocalScheduleDate as R, parseNewsletterSignupAuthoringDestinationValue as S, unsafeAsSiteId as St, assertAllowedPublicUrl as T, unsafeAsStorageBucketName as Tt, toScheduledBookingDateTime as U, toBookingTimeZone as V, parseBookingTimeZone as W, formatCurrencyAmount as X, publicEventPricingType as Y, assertNever as Z, parseFooterCompositionV1 as _, unsafeAsPositiveByteCount as _t, parseVerticalPresetId as a, unsafeAsBookingRequirementId as at, parseMailchimpTagName as b, unsafeAsSafeDownloadFilename as bt, EVENT_PRESET_VALUES as c, unsafeAsDemoCapability as ct, resolvePlanningDateInput as d, unsafeAsEventOfferingId as dt, parseMediaAssetId as et, createSecret as f, unsafeAsEventSeriesId as ft, footerNavigationLayouts as g, unsafeAsOriginalFilename as gt, stableJsonStringify as h, unsafeAsOccurrenceId as ht, parseVerticalId as i, unsafeAsAppointmentResourceId as it, toLocationCoordinates as j, isPresent as k, err as kt, ORDINARY_EVENT_PRESET_VALUES as l, unsafeAsDetectedMimeType as lt, parseManagementKey as m, unsafeAsMediaAssetId as mt, canonicalizeConfiguredBookingVerticalConfig as n, parseSiteId as nt, resolveSiteVerticalBookingConfig as o, unsafeAsBookingSessionId as ot, exposeSecret as p, unsafeAsFileChecksumSha256 as pt, formatEventPricing as q, isEventSeriesVerticalPresetDefinition as r, parseUserId as rt, DEFAULT_EVENT_TYPE_PRESETS as s, unsafeAsDeclaredMimeType as st, areConfiguredBookingVerticalConfigsEqual as t, parsePageId as tt, SURFACE_EVENT_PRESET_VALUES as u, unsafeAsEntitlementId as ut, parseFooterModuleId as v, unsafeAsPositiveSeconds as vt, isCanonicalRichTextDoc as w, unsafeAsStaffMemberId as wt, parseNewsletterListId as x, unsafeAsSiteFontId as xt, parseMailchimpAudienceId as y, unsafeAsProductId as yt, getLocalScheduleTime as z };
6843
+ //#region ../core/src/navigation-locations.ts
6844
+ /** The only places a site's navigation can be displayed. */
6845
+ const NAVIGATION_LOCATIONS = [
6846
+ "main",
6847
+ "footer",
6848
+ "legal"
6849
+ ];
6850
+ const NAVIGATION_LOCATION_DESCRIPTORS = {
6851
+ main: {
6852
+ location: "main",
6853
+ label: "Main navigation",
6854
+ description: "Links shown in the site header when header navigation is enabled."
6855
+ },
6856
+ footer: {
6857
+ location: "footer",
6858
+ label: "Footer links",
6859
+ description: "Links shown in the main footer links section when it is enabled."
6860
+ },
6861
+ legal: {
6862
+ location: "legal",
6863
+ label: "Legal links",
6864
+ description: "Smaller footer links for privacy, terms and other policies."
6865
+ }
6866
+ };
6867
+ function isNavigationLocation(value) {
6868
+ return typeof value === "string" && NAVIGATION_LOCATIONS.includes(value);
6869
+ }
6870
+ /** Parse all rows for one site into a complete fixed-location record. */
6871
+ function parseSiteNavigationLocations(rows) {
6872
+ const locations = {};
6873
+ for (const [index, row] of rows.entries()) {
6874
+ if (!isNavigationLocation(row.location)) {
6875
+ return err({
6876
+ kind: "invalid_location",
6877
+ index,
6878
+ value: row.location
6879
+ });
6880
+ }
6881
+ if (Object.prototype.hasOwnProperty.call(locations, row.location)) {
6882
+ return err({
6883
+ kind: "duplicate_location",
6884
+ location: row.location
6885
+ });
6886
+ }
6887
+ locations[row.location] = row.value;
6888
+ }
6889
+ for (const location of NAVIGATION_LOCATIONS) {
6890
+ if (!Object.prototype.hasOwnProperty.call(locations, location)) {
6891
+ return err({
6892
+ kind: "missing_location",
6893
+ location
6894
+ });
6895
+ }
6896
+ }
6897
+ return ok(locations);
6898
+ }
6899
+
6900
+ //#endregion
6901
+ export { publicEventPricingDisplay as $, assertAllowedPublicUrl as A, unsafeAsSiteFontId as At, getBookingLocalDateKey as B, parseMailingListSignedSourceToken as C, unsafeAsMediaAssetId as Ct, parseFooterCompositionV1 as D, unsafeAsPositiveSeconds as Dt, footerNavigationLayouts as E, unsafeAsPositiveByteCount as Et, formatBookingDate as F, unsafeAsStoragePath as Ft, toScheduledBookingDateRange as G, getLocalScheduleTime as H, formatBookingDateParts as I, unsafeAsUploadSessionId as It, parseEventPageSurfaceSubject as J, toScheduledBookingDateTime as K, formatBookingTime as L, unsafeAsUserId as Lt, normalizeLocationAddressText as M, unsafeAsSiteScopedStoragePath as Mt, toLocationCoordinates as N, unsafeAsStaffMemberId as Nt, parseMailingListPlacementTargeting as O, unsafeAsProductId as Ot, buildBookingUtcDateWindow as P, unsafeAsStorageBucketName as Pt, publicEventPricingCurrency as Q, formatLocalScheduleDate as R, err as Rt, parseMailingListBlockId as S, unsafeAsFileChecksumSha256 as St, planMailingListChoicePresentation as T, unsafeAsOriginalFilename as Tt, parseUtcIsoDateTimeString as U, getLocalScheduleDate as V, toBookingTimeZone as W, formatEventPricing as X, formatBookingPriceDisplay as Y, isPublicEventPricingOptionDisplayable as Z, createSecret as _, unsafeAsDemoCapability as _t, parseVerticalId as a, formatCurrencyAmount as at, stableJsonStringify as b, unsafeAsEventOfferingId as bt, DEFAULT_EVENT_TYPE_PRESETS as c, parseEventSeriesId as ct, SURFACE_EVENT_PRESET_VALUES as d, parseSiteId as dt, publicEventPricingType as et, WHOLE_SERIES_EVENT_PRESET_VALUES as f, parseUserId as ft, resolveWholeSeriesBookingPhase as g, unsafeAsDeclaredMimeType as gt, orderedLiveRunSessions as h, unsafeAsBookingSessionId as ht, isEventSeriesVerticalPresetDefinition as i, mapNonEmptyArray as it, isPresent as j, unsafeAsSiteId as jt, isCanonicalRichTextDoc as k, unsafeAsSafeDownloadFilename as kt, EVENT_PRESET_VALUES as l, parseMediaAssetId as lt, CANCELLED_SESSION_STATUS as m, unsafeAsBookingRequirementId as mt, areConfiguredBookingVerticalConfigsEqual as n, resolvePricingOptionsPriceDisplay as nt, parseVerticalPresetId as o, assertNever as ot, publicReadFreshnessSeconds as p, unsafeAsAppointmentResourceId as pt, parseBookingTimeZone as q, canonicalizeConfiguredBookingVerticalConfig as r, fromArray as rt, resolveSiteVerticalBookingConfig as s, parseApiKeyId as st, NAVIGATION_LOCATIONS as t, resolveExactOrFreePriceDisplay as tt, ORDINARY_EVENT_PRESET_VALUES as u, parsePageId as ut, exposeSecret as v, unsafeAsDetectedMimeType as vt, parseMailingListRuntimeContext as w, unsafeAsOccurrenceId as wt, buildMailingListSignupBlockSourceRef as x, unsafeAsEventSeriesId as xt, parseManagementKey as y, unsafeAsEntitlementId as yt, formatLocalScheduleTime as z, ok as zt };