@designcrowd/library.brand-page 6.2.0 → 6.2.2-live-preview-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-VQCQvlMg.js → CreatedOnBrandCrowd-de-de-Cn0M-eZ1.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-DSf3BjCG.js → CreatedOnBrandCrowd-en-us-DRLMSDZr.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DGcdM9Ec.js → CreatedOnBrandCrowd-es-es-BglTu73U.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-C9g1fc39.js → CreatedOnBrandCrowd-fr-ca-Bj6YZ_el.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-C9g1fc39.js → CreatedOnBrandCrowd-fr-fr-Bj6YZ_el.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-DSXr7Bxl.js → CreatedOnBrandCrowd-pt-br-DEoOmQ1F.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-DSXr7Bxl.js → CreatedOnBrandCrowd-pt-pt-DEoOmQ1F.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-BPdCL6wS.js → CreatedOnDcom-de-de-BZ7gTt4Z.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-CLkSTQRu.js → CreatedOnDcom-en-us-CD6Km5sH.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-CRknMAGv.js → CreatedOnDcom-es-es-CNpZHvEl.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-ClAY_dHI.js → CreatedOnDcom-fr-ca-URS-ydJ1.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DQtv9rR-.js → CreatedOnDcom-fr-fr-D2AVeA3H.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-BbIxvmxv.js → CreatedOnDcom-pt-br-C32frnFs.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-BOfOesBj.js → CreatedOnDcom-pt-pt-TDYGjRXS.js} +1 -1
  15. package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.d.ts +16 -2
  16. package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.js +17 -0
  17. package/lib/src/brand-page/components/page/Page.mixin.d.ts +2 -1
  18. package/lib/src/brand-page/components/page/Page.mixin.js +12 -7
  19. package/lib/src/brand-page/components/page-header/HeaderLayout.stories.d.ts +1 -0
  20. package/lib/src/brand-page/components/page-header/HeaderLayout.stories.js +9 -0
  21. package/lib/src/brand-page/components/page-header/PageHeader.mixin.d.ts +3 -1
  22. package/lib/src/brand-page/components/page-header/PageHeader.mixin.js +10 -4
  23. package/lib/src/brand-page/constants/sections/header-display.constants.d.ts +3 -0
  24. package/lib/src/brand-page/constants/sections/header-display.constants.js +15 -0
  25. package/lib/src/brand-page/models/brand-page-context.model.d.ts +0 -2
  26. package/lib/src/brand-page/models/brand-page-context.model.js +0 -12
  27. package/lib/src/brand-page/models/index.d.ts +12 -0
  28. package/lib/src/brand-page/models/index.js +4 -0
  29. package/lib/src/brand-page/utils/context.util.d.ts +1 -0
  30. package/lib/src/brand-page/utils/context.util.js +9 -0
  31. package/lib/src/brand-page/utils/route.util.d.ts +1 -0
  32. package/lib/src/brand-page/utils/route.util.js +9 -0
  33. package/lib/src/brand-page/utils/view-model.util.js +53 -1
  34. package/lib/src/brand-page/view-models.d.ts +3 -1
  35. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +0 -1
  36. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.js +0 -9
  37. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +9 -44
  38. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +8 -43
  39. package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +0 -14
  40. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +0 -42
  41. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +0 -42
  42. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +0 -42
  43. package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +0 -42
  44. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +0 -42
  45. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +0 -42
  46. package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +3 -0
  47. package/lib/src/brand-page-maker/composables/useEventTracker.js +10 -0
  48. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +1626 -0
  49. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +0 -42
  50. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +2 -4
  51. package/lib/src/brand-page-maker/validators/section-button.validator.d.ts +5 -2
  52. package/lib/src/brand-page-maker/validators/section-button.validator.js +11 -15
  53. package/lib/src/{bundled-translations-B2q5GWTQ.js → bundled-translations-BYUfWBT3.js} +17 -17
  54. package/lib/src/{bundled-translations.de-DE-FhH2H_rd.js → bundled-translations.de-DE-MmzUu1tt.js} +21 -21
  55. package/lib/src/{bundled-translations.es-ES-_ZExxMi7.js → bundled-translations.es-ES-D-tG33XL.js} +5 -5
  56. package/lib/src/{bundled-translations.fr-CA-Tg-ODIiC.js → bundled-translations.fr-CA-CNc1H1h3.js} +1 -1
  57. package/lib/src/{bundled-translations.fr-FR-C0sU51Mz.js → bundled-translations.fr-FR-DX0-yepT.js} +9 -9
  58. package/lib/src/{bundled-translations.pt-BR-MYFIhA4u.js → bundled-translations.pt-BR-ih1-LRgy.js} +9 -9
  59. package/lib/src/{bundled-translations.pt-PT-DeUBSzCB.js → bundled-translations.pt-PT-Dq2CmD1p.js} +22 -22
  60. package/lib/src/bundles/bundled-translations.de-DE.json +9 -2
  61. package/lib/src/bundles/bundled-translations.es-ES.json +9 -2
  62. package/lib/src/bundles/bundled-translations.fr-CA.json +9 -2
  63. package/lib/src/bundles/bundled-translations.fr-FR.json +9 -2
  64. package/lib/src/bundles/bundled-translations.json +9 -2
  65. package/lib/src/bundles/bundled-translations.pt-BR.json +9 -2
  66. package/lib/src/bundles/bundled-translations.pt-PT.json +9 -2
  67. package/lib/src/composables/useTranslate.d.ts +1 -1
  68. package/lib/src/composables/useTranslate.js +1 -1
  69. package/lib/src/css/library.brand-page-brandCrowd.min.css +577 -139
  70. package/lib/src/css/library.brand-page-brandPage.min.css +577 -139
  71. package/lib/src/css/library.brand-page-designCom.min.css +577 -139
  72. package/lib/src/css/library.brand-page-preview.min.css +577 -139
  73. package/lib/src/index.mjs +2 -2
  74. package/lib/src/{lib-BMVF4uNy.js → lib-Dm9AuOJL.js} +22487 -21900
  75. package/lib/src/metadata-builder/builders/item-id-utils.d.ts +2 -0
  76. package/lib/src/metadata-builder/builders/item-id-utils.js +8 -2
  77. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +10 -2
  78. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +7 -1
  79. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +1 -0
  80. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +6 -1
  81. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +6 -43
  82. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +14 -2
  83. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.js +14 -0
  84. package/lib/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.d.ts +9 -1
  85. package/lib/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.js +31 -9
  86. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +1 -0
  87. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +6 -43
  88. package/lib/src/shared/page-navigation/store/page-navigation-store.d.ts +3 -1
  89. package/lib/src/shared/page-navigation/store/page-navigation-store.js +34 -9
  90. package/lib/src/shared/page-settings/components/delete-page-modal/DeletePageModal.mixin.d.ts +1 -1
  91. package/lib/src/shared/page-settings/components/new-page-modal/NewPageModal.mixin.d.ts +9 -2
  92. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +200 -44
  93. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.js +12 -35
  94. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +190 -44
  95. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +34 -2
  96. package/lib/src/shared/page-settings/duplicate-page.util.d.ts +4 -0
  97. package/lib/src/shared/page-settings/duplicate-page.util.js +82 -0
  98. package/lib/src/shared/page-settings/models.d.ts +1 -0
  99. package/lib/src/shared/page-settings/store/page-settings-store.d.ts +7 -2
  100. package/lib/src/shared/page-settings/store/page-settings-store.js +90 -7
  101. package/lib/src/shared/page-settings/utils.d.ts +3 -0
  102. package/lib/src/shared/page-settings/utils.js +6 -0
  103. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +1 -56
  104. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +0 -42
  105. package/lib/src/shared/side-bar/components/SideBarTabButton.mixin.d.ts +1 -0
  106. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +4 -42
  107. package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +1 -0
  108. package/lib/src/tracker.mjs +1 -1
  109. package/lib/src/{useEventTracker-C5Ql5p7u.js → useEventTracker-DwPTmzpa.js} +13 -3
  110. package/package.json +2 -2
  111. package/src/brand-page/components/navigation/hamburger/HamburgerNavigation.vue +1 -0
  112. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts +18 -1
  113. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue +73 -3
  114. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue.test.ts +181 -0
  115. package/src/brand-page/components/page/Page.mixin.ts +21 -11
  116. package/src/brand-page/components/page/Page.vue +2 -0
  117. package/src/brand-page/components/page-header/HeaderLayout.stories.ts +10 -0
  118. package/src/brand-page/components/page-header/HeaderLayout.vue +415 -71
  119. package/src/brand-page/components/page-header/PageHeader.mixin.ts +14 -10
  120. package/src/brand-page/components/page-header/PageHeader.vue +16 -1
  121. package/src/brand-page/components/page-header/PageHeader.vue.test.ts +339 -0
  122. package/src/brand-page/constants/sections/header-display.constants.test.ts +44 -0
  123. package/src/brand-page/constants/sections/header-display.constants.ts +35 -0
  124. package/src/brand-page/models/brand-page-context.model.ts +0 -23
  125. package/src/brand-page/models/header-button.model.test.ts +85 -0
  126. package/src/brand-page/models/index.ts +19 -0
  127. package/src/brand-page/utils/context.util.ts +15 -0
  128. package/src/brand-page/utils/route.util.ts +13 -0
  129. package/src/brand-page/utils/view-model.util.test.ts +151 -1
  130. package/src/brand-page/utils/view-model.util.ts +73 -1
  131. package/src/brand-page/view-models.ts +3 -0
  132. package/src/brand-page-maker/components/action-bar/ActionBar.mixin.ts +0 -10
  133. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.test.ts +8 -49
  134. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.vue +1 -1
  135. package/src/brand-page-maker/composables/useEventTracker.ts +13 -0
  136. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +2 -4
  137. package/src/brand-page-maker/validators/section-button.validator.ts +23 -19
  138. package/src/bundles/bundled-translations.de-DE.json +9 -2
  139. package/src/bundles/bundled-translations.es-ES.json +9 -2
  140. package/src/bundles/bundled-translations.fr-CA.json +9 -2
  141. package/src/bundles/bundled-translations.fr-FR.json +9 -2
  142. package/src/bundles/bundled-translations.json +9 -2
  143. package/src/bundles/bundled-translations.pt-BR.json +9 -2
  144. package/src/bundles/bundled-translations.pt-PT.json +9 -2
  145. package/src/composables/useTranslate.ts +2 -1
  146. package/src/metadata-builder/builders/item-id-utils.ts +9 -2
  147. package/src/shared/content-settings/components/text-field/TextField.mixin.ts +15 -0
  148. package/src/shared/content-settings/components/url-field/UrlField.vue +1 -0
  149. package/src/shared/ecommerce-settings/components/product-config-field/ProductConfigOption.vue +0 -2
  150. package/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.ts +32 -10
  151. package/src/shared/page-navigation/store/page-navigation-store.ts +57 -13
  152. package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.button-section.test.ts +131 -0
  153. package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.vue +65 -10
  154. package/src/shared/page-settings/components/page-settings/NestedDraggable.vue +3 -0
  155. package/src/shared/page-settings/components/page-settings/PageElement.vue +2 -0
  156. package/src/shared/page-settings/components/page-settings/PageSettingMenu.test.ts +105 -0
  157. package/src/shared/page-settings/components/page-settings/PageSettingMenu.vue +16 -0
  158. package/src/shared/page-settings/components/page-settings/PageSettings.duplicate.test.ts +108 -0
  159. package/src/shared/page-settings/components/page-settings/PageSettings.mixin.ts +22 -50
  160. package/src/shared/page-settings/components/page-settings/PageSettings.vue +1 -0
  161. package/src/shared/page-settings/components/page-settings/i18n/page-settings.de-DE.json +3 -1
  162. package/src/shared/page-settings/components/page-settings/i18n/page-settings.es-ES.json +3 -1
  163. package/src/shared/page-settings/components/page-settings/i18n/page-settings.fr-CA.json +3 -1
  164. package/src/shared/page-settings/components/page-settings/i18n/page-settings.fr-FR.json +3 -1
  165. package/src/shared/page-settings/components/page-settings/i18n/page-settings.json +3 -1
  166. package/src/shared/page-settings/components/page-settings/i18n/page-settings.pt-BR.json +3 -1
  167. package/src/shared/page-settings/components/page-settings/i18n/page-settings.pt-PT.json +3 -1
  168. package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.test.ts +305 -0
  169. package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.vue +265 -0
  170. package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.test.ts +354 -0
  171. package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.vue +48 -9
  172. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.header-button.test.ts +114 -0
  173. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +38 -1
  174. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.de-DE.json +6 -1
  175. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.es-ES.json +6 -1
  176. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.fr-CA.json +6 -1
  177. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.fr-FR.json +6 -1
  178. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.json +6 -1
  179. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.pt-BR.json +6 -1
  180. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.pt-PT.json +6 -1
  181. package/src/shared/page-settings/duplicate-page.util.test.ts +382 -0
  182. package/src/shared/page-settings/duplicate-page.util.ts +124 -0
  183. package/src/shared/page-settings/models.ts +1 -0
  184. package/src/shared/page-settings/store/page-settings-store.test.ts +441 -1
  185. package/src/shared/page-settings/store/page-settings-store.ts +147 -11
  186. package/src/shared/page-settings/utils.ts +10 -0
@@ -0,0 +1,85 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import {
3
+ BrandPageModel,
4
+ BUTTON_LINK_TYPE,
5
+ PageHeaderDesktop,
6
+ PageHeaderMobile,
7
+ type HeaderButton,
8
+ } from './index';
9
+
10
+ const aHeaderButton = (): HeaderButton => ({
11
+ isVisible: true,
12
+ text: 'Contact Us',
13
+ buttonLinkType: BUTTON_LINK_TYPE.url,
14
+ url: 'https://example.com',
15
+ buttonTextStyle: { textStyleType: 'buttons' },
16
+ fillColor: '#123456',
17
+ borderColor: '#654321',
18
+ });
19
+
20
+ // The constructors copy fields one-by-one — a forgotten field is silently dropped on load.
21
+ describe('header button persistence', () => {
22
+ describe('PageHeader* constructor', () => {
23
+ test('keeps a persisted headerButton on both viewports', () => {
24
+ expect(
25
+ new PageHeaderDesktop({ headerButton: aHeaderButton() }).headerButton,
26
+ ).toEqual(aHeaderButton());
27
+ expect(
28
+ new PageHeaderMobile({ headerButton: aHeaderButton() }).headerButton,
29
+ ).toEqual(aHeaderButton());
30
+ });
31
+
32
+ test('leaves headerButton unset when absent (no default seeded)', () => {
33
+ expect(new PageHeaderDesktop().headerButton).toBeUndefined();
34
+ expect(new PageHeaderMobile().headerButton).toBeUndefined();
35
+ });
36
+
37
+ test('keeps a page link button with buttonPageId', () => {
38
+ const button: HeaderButton = {
39
+ isVisible: true,
40
+ text: 'About',
41
+ buttonLinkType: BUTTON_LINK_TYPE.page,
42
+ buttonPageId: 'page-2',
43
+ };
44
+
45
+ expect(
46
+ new PageHeaderDesktop({ headerButton: button }).headerButton,
47
+ ).toEqual(button);
48
+ });
49
+ });
50
+
51
+ describe('BrandPageModel round-trip', () => {
52
+ test('survives deserialization from persisted JSON on both viewports', () => {
53
+ const model = new BrandPageModel({
54
+ styles: {},
55
+ pages: [],
56
+ pageHeaderDesktop: { headerButton: aHeaderButton() },
57
+ pageHeaderMobile: { headerButton: aHeaderButton() },
58
+ });
59
+
60
+ expect(model.pageHeaderDesktop.headerButton).toEqual(aHeaderButton());
61
+ expect(model.pageHeaderMobile.headerButton).toEqual(aHeaderButton());
62
+ });
63
+
64
+ test('survives a second pass through the model (re-serialize then re-hydrate)', () => {
65
+ const first = new BrandPageModel({
66
+ styles: {},
67
+ pages: [],
68
+ pageHeaderDesktop: { headerButton: aHeaderButton() },
69
+ pageHeaderMobile: { headerButton: aHeaderButton() },
70
+ });
71
+
72
+ const second = new BrandPageModel(JSON.parse(JSON.stringify(first)));
73
+
74
+ expect(second.pageHeaderDesktop.headerButton).toEqual(aHeaderButton());
75
+ expect(second.pageHeaderMobile.headerButton).toEqual(aHeaderButton());
76
+ });
77
+
78
+ test('an existing site with no headerButton stays without one', () => {
79
+ const model = new BrandPageModel({ styles: {}, pages: [] });
80
+
81
+ expect(model.pageHeaderDesktop.headerButton).toBeUndefined();
82
+ expect(model.pageHeaderMobile.headerButton).toBeUndefined();
83
+ });
84
+ });
85
+ });
@@ -713,6 +713,17 @@ export class NavModel {
713
713
  }
714
714
  }
715
715
 
716
+ export interface HeaderButton {
717
+ isVisible: boolean;
718
+ text: string;
719
+ buttonLinkType: ButtonLinkType;
720
+ url?: string;
721
+ buttonPageId?: string;
722
+ buttonTextStyle?: TextStyleDeclaration;
723
+ fillColor?: string;
724
+ borderColor?: string;
725
+ }
726
+
716
727
  type AbstractPageHeaderStylesType = Partial<AbstractPageHeaderStyles>;
717
728
 
718
729
  export abstract class AbstractPageHeaderStyles {
@@ -742,6 +753,9 @@ export abstract class AbstractPageHeaderStyles {
742
753
  style: BrandPageCssStyle;
743
754
  viewport: ViewportsType;
744
755
 
756
+ /** Missing = no button. `declare` keeps it off saved data so old migrations still see the same shape. */
757
+ declare headerButton?: HeaderButton;
758
+
745
759
  protected constructor(options: AbstractPageHeaderStylesType = {}) {
746
760
  if (typeof options.viewport === 'undefined') {
747
761
  throw new Error('"viewport" is required');
@@ -779,6 +793,11 @@ export abstract class AbstractPageHeaderStyles {
779
793
  ...(options.backgroundStyle || {}),
780
794
  backgroundOpacity: options.backgroundStyle?.backgroundOpacity ?? 100,
781
795
  };
796
+
797
+ // Only set when given — a missing button must stay missing for old migrations.
798
+ if (options.headerButton) {
799
+ this.headerButton = options.headerButton;
800
+ }
782
801
  }
783
802
  }
784
803
 
@@ -17,3 +17,18 @@ export const getSlugFromContext = (context: BrandPageContext) => {
17
17
  */
18
18
  export const isPreviewMode = (context: BrandPageContext): boolean =>
19
19
  !!(context.isPreview || context.isLivePreview);
20
+
21
+ // Blocks a click in the maker preview (no link-follow, no bubbling); true when it fired.
22
+ export const guardPreviewClick = (
23
+ event: MouseEvent,
24
+ context: BrandPageContext,
25
+ ): boolean => {
26
+ if (!context.isPreview) {
27
+ return false;
28
+ }
29
+
30
+ event.preventDefault();
31
+ event.stopPropagation();
32
+
33
+ return true;
34
+ };
@@ -208,3 +208,16 @@ export const findNavItemByPageSlug = (
208
208
 
209
209
  return undefined;
210
210
  };
211
+
212
+ // Nav shows when more than one page is visible, or one page has visible subpages.
213
+ // The header row and the side menu both key the button placement off this — one
214
+ // source so the two can never disagree.
215
+ export const hasVisibleNavigation = (navItems: NavItemViewModel[]): boolean => {
216
+ const visibleNavItems = navItems.filter((item) => !item.isHidden);
217
+
218
+ return (
219
+ visibleNavItems.length > 1 ||
220
+ (visibleNavItems.length === 1 &&
221
+ visibleNavItems[0].nestedNavItems.some((item) => !item.isHidden))
222
+ );
223
+ };
@@ -5,8 +5,14 @@ import {
5
5
  type MailingListContent,
6
6
  type ContactFormContent,
7
7
  type ColorItem,
8
+ type HeaderButton,
9
+ } from '../models';
10
+ import {
11
+ BRAND_PAGE_TYPES,
12
+ BUTTON_LINK_TYPE,
13
+ PageHeaderDesktop,
14
+ PageStyles,
8
15
  } from '../models';
9
- import { BRAND_PAGE_TYPES, PageHeaderDesktop, PageStyles } from '../models';
10
16
  import {
11
17
  mapToBannerSectionViewModel,
12
18
  mapToContactFormViewModel,
@@ -1114,3 +1120,147 @@ describe('container-aware text colour defaults (view-model layer)', () => {
1114
1120
  expect(vm.nameStyle?.color).toBe('#00ff00');
1115
1121
  });
1116
1122
  });
1123
+
1124
+ describe('mapToPageHeaderViewModel header button', () => {
1125
+ const headerButton: HeaderButton = {
1126
+ isVisible: true,
1127
+ text: 'Contact Us',
1128
+ buttonLinkType: BUTTON_LINK_TYPE.url,
1129
+ url: 'https://example.com',
1130
+ buttonTextStyle: { textStyleType: 'buttons' },
1131
+ fillColor: '#123456',
1132
+ borderColor: '#654321',
1133
+ };
1134
+
1135
+ describe('headerButtonViewModel', () => {
1136
+ const mapButton = (overrides: Partial<HeaderButton> = {}) =>
1137
+ mapToPageHeaderViewModel(
1138
+ new PageHeaderDesktop({
1139
+ headerButton: { ...headerButton, ...overrides },
1140
+ }),
1141
+ new PageStyles(),
1142
+ [],
1143
+ ).headerButtonViewModel;
1144
+
1145
+ test('is absent when the header has no button at all', () => {
1146
+ const vm = mapToPageHeaderViewModel(
1147
+ new PageHeaderDesktop(),
1148
+ new PageStyles(),
1149
+ [],
1150
+ );
1151
+
1152
+ expect(vm.headerButtonViewModel).toBeUndefined();
1153
+ });
1154
+
1155
+ test('is absent when the button is configured but hidden', () => {
1156
+ expect(mapButton({ isVisible: false })).toBeUndefined();
1157
+ });
1158
+
1159
+ test('carries the authored label', () => {
1160
+ expect(mapButton({ text: 'Book now' })?.title).toBe('Book now');
1161
+ });
1162
+
1163
+ // Wrapped lines justify; CSS text-align-last keeps the only/last line centered.
1164
+ test('the label text-align is justify', () => {
1165
+ expect(mapButton({})?.titleStyle.textAlign).toBe('justify');
1166
+ });
1167
+
1168
+ // No label means no button — the maker seeds "Contact Us", so this is a deliberate clear.
1169
+ test.each([[''], [' ']])(
1170
+ 'a blank label hides the button (%j)',
1171
+ (text) => {
1172
+ expect(mapButton({ text })).toBeUndefined();
1173
+ },
1174
+ );
1175
+
1176
+ // Button.vue reads the link kind off `role` for non-section content, so the
1177
+ // authored link type has to land on the role/clickActionField pair.
1178
+ test.each([
1179
+ [BUTTON_LINK_TYPE.url, 'general-link', 'url'],
1180
+ [BUTTON_LINK_TYPE.page, 'general-link', 'page'],
1181
+ [BUTTON_LINK_TYPE.mailTo, 'email', 'url'],
1182
+ [BUTTON_LINK_TYPE.tel, 'phone', 'url'],
1183
+ ])(
1184
+ 'maps the %s link type to role %s / clickActionField %s',
1185
+ (buttonLinkType, role, clickActionField) => {
1186
+ const vm = mapButton({ buttonLinkType });
1187
+
1188
+ expect(vm?.role).toBe(role);
1189
+ expect(vm?.clickActionField).toBe(clickActionField);
1190
+ },
1191
+ );
1192
+
1193
+ test('routes a page link through the picked page id', () => {
1194
+ const vm = mapButton({
1195
+ buttonLinkType: BUTTON_LINK_TYPE.page,
1196
+ buttonPageId: 'page-2',
1197
+ url: undefined,
1198
+ });
1199
+
1200
+ expect(vm?.page).toBe('page-2');
1201
+ expect(vm?.pageSlug).toBe('page-2');
1202
+ });
1203
+
1204
+ test('overrides the theme fill and border with the authored colours', () => {
1205
+ const vm = mapButton({ fillColor: '#123456', borderColor: '#654321' });
1206
+
1207
+ expect(vm?.styles.style.backgroundColor).toBe('#123456');
1208
+ expect(vm?.styles.style.borderColor).toBe('#654321');
1209
+ });
1210
+
1211
+ test('keeps the site button style when no colours are authored', () => {
1212
+ const styles = new PageStyles();
1213
+ const vm = mapToPageHeaderViewModel(
1214
+ new PageHeaderDesktop({
1215
+ headerButton: {
1216
+ ...headerButton,
1217
+ fillColor: undefined,
1218
+ borderColor: undefined,
1219
+ },
1220
+ }),
1221
+ styles,
1222
+ [],
1223
+ ).headerButtonViewModel;
1224
+
1225
+ expect(vm?.styles.style.backgroundColor).toBe(
1226
+ styles.buttonStyles.style.backgroundColor,
1227
+ );
1228
+ expect(vm?.styles.selectedButtonStyleId).toBe(
1229
+ styles.buttonStyles.selectedButtonStyleId,
1230
+ );
1231
+ });
1232
+
1233
+ test('resolves the button text style against the site text styles', () => {
1234
+ const styles = new PageStyles();
1235
+
1236
+ styles.textStyles.textStyleOptions.buttons.style.fontFamily = 'Lato';
1237
+
1238
+ const vm = mapToPageHeaderViewModel(
1239
+ new PageHeaderDesktop({ headerButton }),
1240
+ styles,
1241
+ [],
1242
+ ).headerButtonViewModel;
1243
+
1244
+ expect(vm?.titleStyle?.fontFamily).toBe('Lato');
1245
+ });
1246
+
1247
+ // The maker writes buttonTextStyle only once the author touches the style field,
1248
+ // so an untouched button still has to inherit the site's button text style.
1249
+ test('defaults to the site button text style when none is authored', () => {
1250
+ const styles = new PageStyles();
1251
+
1252
+ styles.textStyles.textStyleOptions.buttons.style.fontFamily = 'Lato';
1253
+
1254
+ const vm = mapToPageHeaderViewModel(
1255
+ new PageHeaderDesktop({
1256
+ headerButton: { ...headerButton, buttonTextStyle: undefined },
1257
+ }),
1258
+ styles,
1259
+ [],
1260
+ ).headerButtonViewModel;
1261
+
1262
+ expect(vm?.titleStyle?.textStyleType).toBe('buttons');
1263
+ expect(vm?.titleStyle?.fontFamily).toBe('Lato');
1264
+ });
1265
+ });
1266
+ });
@@ -44,8 +44,11 @@ import type {
44
44
  NavRoute,
45
45
  NavOrder,
46
46
  ColorItem,
47
+ HeaderButton,
48
+ ButtonLinkType,
49
+ ContentType,
47
50
  } from '../models';
48
- import { ButtonStyles, VIEWPORTS } from '../models';
51
+ import { ButtonStyles, TEXT_STYLE_TYPES, VIEWPORTS } from '../models';
49
52
  import {
50
53
  NavigationMobile,
51
54
  NavigationDesktop,
@@ -1189,6 +1192,71 @@ export const mapToPageStyleViewModel = (
1189
1192
  };
1190
1193
  };
1191
1194
 
1195
+ // Maps each link kind to the role Button.vue already understands, so the button needs no new code.
1196
+ const HEADER_BUTTON_LINK_ROLES: Record<ButtonLinkType, ContentType> = {
1197
+ [BUTTON_LINK_TYPE.url]: 'general-link',
1198
+ [BUTTON_LINK_TYPE.page]: 'general-link',
1199
+ [BUTTON_LINK_TYPE.mailTo]: 'email',
1200
+ [BUTTON_LINK_TYPE.tel]: 'phone',
1201
+ };
1202
+
1203
+ const mapToHeaderButtonViewModel = (
1204
+ headerButton: HeaderButton | undefined,
1205
+ styles: PageStyles,
1206
+ ): ButtonViewModel | undefined => {
1207
+ // No label means no button — hidden in preview and on the published site.
1208
+ if (!headerButton?.isVisible || !headerButton.text?.trim()) {
1209
+ return undefined;
1210
+ }
1211
+
1212
+ const titleStyle: TextStyleDeclaration = headerButton.buttonTextStyle ?? {
1213
+ textStyleType: TEXT_STYLE_TYPES.buttons,
1214
+ };
1215
+
1216
+ const viewModel = mapToButtonViewModel(
1217
+ {
1218
+ id: 'page-header-button',
1219
+ // Old saved data may have no link kind — treat it as a url.
1220
+ type:
1221
+ HEADER_BUTTON_LINK_ROLES[headerButton.buttonLinkType] ??
1222
+ HEADER_BUTTON_LINK_ROLES[BUTTON_LINK_TYPE.url],
1223
+ iconUrl: '',
1224
+ modalImageUrl: '',
1225
+ colorReplacements: [],
1226
+ title: headerButton.text.trim(),
1227
+ titleStyle,
1228
+ url: headerButton.url ?? '',
1229
+ page: headerButton.buttonPageId ?? '',
1230
+ clickActionField:
1231
+ headerButton.buttonLinkType === BUTTON_LINK_TYPE.page ? 'page' : 'url',
1232
+ },
1233
+ styles,
1234
+ { titleStyle },
1235
+ );
1236
+
1237
+ return {
1238
+ ...viewModel,
1239
+ // A wrapped label justifies; the last (or only) line stays centered like a short label.
1240
+ titleStyle: {
1241
+ ...viewModel.titleStyle,
1242
+ textAlign: 'justify',
1243
+ textAlignLast: 'center',
1244
+ },
1245
+ styles: {
1246
+ ...viewModel.styles,
1247
+ style: {
1248
+ ...viewModel.styles.style,
1249
+ ...(headerButton.fillColor
1250
+ ? { backgroundColor: headerButton.fillColor }
1251
+ : {}),
1252
+ ...(headerButton.borderColor
1253
+ ? { borderColor: headerButton.borderColor }
1254
+ : {}),
1255
+ },
1256
+ },
1257
+ };
1258
+ };
1259
+
1192
1260
  export const mapToPageHeaderViewModel = (
1193
1261
  pageHeader: PageHeaderDesktop | PageHeaderMobile,
1194
1262
  pageStyles: PageStyles,
@@ -1283,6 +1351,10 @@ export const mapToPageHeaderViewModel = (
1283
1351
  }),
1284
1352
  style: pageHeader.style,
1285
1353
  backgroundStyle: getHeaderBackgroundStyleWithOpacity(pageHeader),
1354
+ headerButtonViewModel: mapToHeaderButtonViewModel(
1355
+ pageHeader.headerButton,
1356
+ pageStyles,
1357
+ ),
1286
1358
  };
1287
1359
  };
1288
1360
 
@@ -259,11 +259,14 @@ export interface PageHeaderViewModel extends Omit<
259
259
  | 'logoTemplateImage'
260
260
  | 'logoImageHeight'
261
261
  | 'logoImageWidth'
262
+ | 'headerButton'
262
263
  | 'backgroundStyle'
263
264
  > {
264
265
  logo: LogoViewModel;
265
266
  pages: PageViewModel[];
266
267
  backgroundStyle: BrandPageCssStyle;
268
+ /** Absent unless the header call-to-action button is configured AND visible. */
269
+ headerButtonViewModel?: ButtonViewModel;
267
270
  }
268
271
 
269
272
  export interface PageFooterViewModel extends Omit<
@@ -17,16 +17,6 @@ export default defineComponent({
17
17
 
18
18
  return {
19
19
  isAdminMaker: computed(() => containerStore.isAdminMaker),
20
- // BP-5873: the Preview button is behind an AB test, and pre/post purchase are
21
- // two separate experiments. Pick the one for the current purchase state — a
22
- // user's value on the other side is independent and must not leak across.
23
- isLivePreviewEnabled: computed(() => {
24
- const experiment = containerStore.isPrePurchase
25
- ? containerStore.abTests.bp_5873_live_preview_prepurchase
26
- : containerStore.abTests.bp_5873_live_preview_postpurchase;
27
-
28
- return !!experiment?.isVariation;
29
- }),
30
20
  isSaving: computed(() => brandPageMakerStore.isSaving),
31
21
  isPublished: computed(() => brandPageMakerStore.isPublished),
32
22
  isPublishButtonEnabled: computed(
@@ -3,31 +3,13 @@ import { createPinia, setActivePinia } from 'pinia';
3
3
  import { shallowMount } from '@vue/test-utils';
4
4
  import ActionBarNavigation from './ActionBarNavigation.vue';
5
5
  import { useBrandPageContainerStore } from '@brand-page-maker/stores';
6
- import { getAbTestResults } from '@brand-page/models/brand-page-context.model';
7
-
8
- // Must match `CampaignNames.LivePreview*` in bc-net. The two experiments share
9
- // one set of variation values — they're told apart by campaign key, not value.
10
- const PREPURCHASE = 'bc-bp-5873-live-preview-prepurchase';
11
- const POSTPURCHASE = 'bc-bp-5873-live-preview-postpurchase';
12
- const VARIATION_1 = 'bp-5873-variation-1';
13
- const CONTROL = 'bp-5873-control';
14
6
 
15
7
  const mountBar = () => shallowMount(ActionBarNavigation);
16
8
 
17
9
  const hasPreviewButton = (wrapper: ReturnType<typeof mountBar>) =>
18
10
  wrapper.find('[data-test-preview-button]').exists();
19
11
 
20
- // The AB value arrives as the raw bc-net campaign string, so go through the real
21
- // parser rather than hand-building an AbTestResult — that's the contract under test.
22
- const setAbTest = (
23
- store: ReturnType<typeof useBrandPageContainerStore>,
24
- key: string,
25
- value: string,
26
- ) => {
27
- store.abTests = getAbTestResults({ [key]: value } as never);
28
- };
29
-
30
- describe('ActionBarNavigation — BP-5873 preview AB test', () => {
12
+ describe('ActionBarNavigation — Preview button', () => {
31
13
  let containerStore: ReturnType<typeof useBrandPageContainerStore>;
32
14
 
33
15
  beforeEach(() => {
@@ -35,39 +17,18 @@ describe('ActionBarNavigation — BP-5873 preview AB test', () => {
35
17
  containerStore = useBrandPageContainerStore();
36
18
  });
37
19
 
38
- test('hides Preview when no AB value has been sent', () => {
39
- expect(hasPreviewButton(mountBar())).toBe(false);
40
- });
41
-
42
- test('post-purchase: shows Preview on variation-1, hides it on control', () => {
43
- containerStore.isPrePurchase = false;
44
-
45
- setAbTest(containerStore, POSTPURCHASE, VARIATION_1);
46
- expect(hasPreviewButton(mountBar())).toBe(true);
47
-
48
- setAbTest(containerStore, POSTPURCHASE, CONTROL);
49
- expect(hasPreviewButton(mountBar())).toBe(false);
50
- });
51
-
52
- test('pre-purchase: shows Preview on variation-1, hides it on control', () => {
53
- containerStore.isPrePurchase = true;
20
+ test.each([
21
+ ['pre-purchase', true],
22
+ ['post-purchase', false],
23
+ ])('shows Preview to the customer %s', (_, isPrePurchase) => {
24
+ containerStore.isPrePurchase = isPrePurchase;
54
25
 
55
- setAbTest(containerStore, PREPURCHASE, VARIATION_1);
56
26
  expect(hasPreviewButton(mountBar())).toBe(true);
57
-
58
- setAbTest(containerStore, PREPURCHASE, CONTROL);
59
- expect(hasPreviewButton(mountBar())).toBe(false);
60
27
  });
61
28
 
62
- // Two separate experiments, so a value on one side must never satisfy the other —
63
- // otherwise a post-purchase variation would leak the button into pre-purchase.
64
- test('the two experiments never read each other', () => {
65
- containerStore.isPrePurchase = true;
66
- setAbTest(containerStore, POSTPURCHASE, VARIATION_1);
67
- expect(hasPreviewButton(mountBar())).toBe(false);
29
+ test('hides Preview in the admin maker', () => {
30
+ containerStore.isAdminMaker = true;
68
31
 
69
- containerStore.isPrePurchase = false;
70
- setAbTest(containerStore, PREPURCHASE, VARIATION_1);
71
32
  expect(hasPreviewButton(mountBar())).toBe(false);
72
33
  });
73
34
 
@@ -75,8 +36,6 @@ describe('ActionBarNavigation — BP-5873 preview AB test', () => {
75
36
  // Save drops out. Preview must be a real column like the rest — while it was content
76
37
  // width, Publish ate the whole remaining row on a published site.
77
38
  test('Preview shares the mobile row equally with the other buttons', () => {
78
- containerStore.isPrePurchase = false;
79
- setAbTest(containerStore, POSTPURCHASE, VARIATION_1);
80
39
  containerStore.brandPageStatus = 'Published';
81
40
 
82
41
  const wrapper = mountBar();
@@ -4,7 +4,7 @@
4
4
  class="tw-flex tw-items-center tw-border-t tw-border-solid tw-border-grayscale-600 tw-h-12 tw-px-2 lg:tw-px-0 lg:tw-h-auto lg:tw-border-none tw-gap-3 lg:tw-gap-5 tw-w-full lg:tw-justify-end tw-box-border"
5
5
  >
6
6
  <Button
7
- v-if="!isAdminMaker && isLivePreviewEnabled"
7
+ v-if="!isAdminMaker"
8
8
  :label="constantsTr('previewDisplayName')"
9
9
  variant="no-border"
10
10
  :white="true"
@@ -39,6 +39,7 @@ export const EVENTS = {
39
39
  WEBSITE_MAKER_ADD_PAGE: 'website_maker_add_page',
40
40
  WEBSITE_MAKER_REMOVE_PAGE: 'website_maker_remove_page',
41
41
  WEBSITE_MAKER_REORDER_PAGE: 'website_maker_reorder_page',
42
+ WEBSITE_MAKER_DUPLICATE_PAGE: 'website_maker_duplicate_page',
42
43
  WEBSITE_MAKER_OPEN: 'website_maker_open',
43
44
  WEBSITE_MAKER_PUBLISH_UPSELL_OPTION_MODAL:
44
45
  'website_maker_publish_upsell_option_modal',
@@ -107,6 +108,7 @@ export const UPGRADE_CTA_LOCATIONS = {
107
108
  REMOVE_WATERMARK_FOOTER_SETTINGS: 'remove_watermark_footer_settings',
108
109
  PREVIEW_WATERMARK: 'preview_watermark',
109
110
  ADD_NEW_PAGE: 'add_new_page',
111
+ DUPLICATE_PAGE: 'duplicate_page',
110
112
  ADD_CONTENT: 'add_content',
111
113
  EDITOR_PUBLISH_TOP: 'editor_publish_top',
112
114
  EDITOR_PUBLISH_BOTTOM: 'editor_publish_bottom',
@@ -346,6 +348,17 @@ const useEventTracker = () => {
346
348
  page_name: pageName,
347
349
  });
348
350
  },
351
+ // The SOURCE page, not the copy: tracking the copy would fragment the
352
+ // dimension across "About Copy", "About Copy 2".
353
+ sendWebsiteManageDuplicatePageEvent: (
354
+ sourcePageName: string,
355
+ isSubPage?: boolean,
356
+ ) => {
357
+ _publishEvent(EVENTS.WEBSITE_MAKER_DUPLICATE_PAGE, {
358
+ page_name: sourcePageName,
359
+ is_sub_page: Boolean(isSubPage),
360
+ });
361
+ },
349
362
  sendPublishOptionsForFreeEvent: () => {
350
363
  _publishEvent(EVENTS.WEBSITE_MAKER_PUBLISH_UPSELL_OPTION_MODAL, {
351
364
  upsell_option_continuation:
@@ -93,6 +93,7 @@ import {
93
93
  UI_SCREENSIZE_MODES,
94
94
  } from '@shared/models';
95
95
  import { PAGE_SECTIONS } from '@shared/page-settings/models';
96
+ import { toPageModels } from '@shared/page-settings/utils';
96
97
  import { useUiTemplateSettingsStore } from '@shared/template-settings/store';
97
98
  import { useBrandPageContainerStore } from '../brandpage-container';
98
99
  import { useLocalisedRouteStore } from '../route/localised-route-store';
@@ -1924,10 +1925,7 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
1924
1925
  const pageNavigationStore = usePageNavigationStore();
1925
1926
  const paletteStore = useUiColorPaletteStore();
1926
1927
 
1927
- const pages = pageSettingsStore.pages.map((p) => ({
1928
- ...p,
1929
- contents: p.contents.map((c) => c.settings.content),
1930
- })) as PageModel[];
1928
+ const pages = toPageModels(pageSettingsStore.pages);
1931
1929
 
1932
1930
  const backgroundStyles = backgroundSettingsStore.backgroundStyles;
1933
1931
  const logoStyles = styleSettingsStore.logoStyles;
@@ -38,14 +38,15 @@ export const textValidator: ContentValidator<BannerSectionContent> = (
38
38
  };
39
39
  };
40
40
 
41
- export const urlValidator: ContentValidator<
42
- BannerSectionContent | BookingSectionContent
43
- > = (section): ValidateResult => {
41
+ const VALID: ValidateResult = { message: '', isValid: true };
42
+
43
+ // The link rules alone — the header button uses these directly (its link is always required).
44
+ export const buttonLinkValidator = (
45
+ section: Pick<BannerSectionContent, 'buttonLinkType' | 'url'>,
46
+ ): ValidateResult => {
44
47
  if (
45
48
  section.buttonLinkType === BUTTON_LINK_TYPE.url &&
46
- (!isURL(section.url) || containsAtSymbol(section.url)) &&
47
- section.buttonText !== false &&
48
- section.buttonText?.length > 0
49
+ (!isURL(section.url) || containsAtSymbol(section.url))
49
50
  ) {
50
51
  return {
51
52
  message: MISSING_BUTTON_URL_MESSAGE(),
@@ -56,9 +57,7 @@ export const urlValidator: ContentValidator<
56
57
  if (
57
58
  section.buttonLinkType === BUTTON_LINK_TYPE.mailTo &&
58
59
  typeof section.url === 'string' &&
59
- !isEmail(section.url) &&
60
- section.buttonText !== false &&
61
- section.buttonText?.length > 0
60
+ !isEmail(section.url)
62
61
  ) {
63
62
  return {
64
63
  message: MISSING_BUTTON_EMAIL_MESSAGE(),
@@ -66,24 +65,29 @@ export const urlValidator: ContentValidator<
66
65
  };
67
66
  }
68
67
 
69
- if (
70
- section.buttonLinkType === BUTTON_LINK_TYPE.tel &&
71
- !isTel(section.url) &&
72
- section.buttonText !== false &&
73
- section.buttonText?.length > 0
74
- ) {
68
+ if (section.buttonLinkType === BUTTON_LINK_TYPE.tel && !isTel(section.url)) {
75
69
  return {
76
70
  message: MISSING_BUTTON_TEL_MESSAGE(),
77
71
  isValid: false,
78
72
  };
79
73
  }
80
74
 
81
- return {
82
- message: '',
83
- isValid: true,
84
- };
75
+ return VALID;
85
76
  };
86
77
 
78
+ type ButtonLinkFields = Pick<
79
+ BannerSectionContent,
80
+ 'buttonLinkType' | 'url' | 'buttonText'
81
+ >;
82
+
83
+ // The banner's rule: its button text is optional, and only a button with text needs a link.
84
+ export const urlValidator: ContentValidator<ButtonLinkFields> = (
85
+ section,
86
+ ): ValidateResult =>
87
+ section.buttonText !== false && section.buttonText?.length > 0
88
+ ? buttonLinkValidator(section)
89
+ : VALID;
90
+
87
91
  export const pageValidator: ContentValidator<
88
92
  BannerSectionContent | BookingSectionContent
89
93
  > = (section): ValidateResult => {