@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,305 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest';
2
+ import { createPinia, setActivePinia } from 'pinia';
3
+ import { shallowMount } from '@vue/test-utils';
4
+ import type { VueWrapper } from '@vue/test-utils';
5
+ import type { HeaderButton, LogoTheme } from '@brand-page';
6
+ import HeaderButtonSettings from './HeaderButtonSettings.vue';
7
+ import { useUiPageSettingsStore } from '../../store';
8
+ import { useUiStyleSettingsStore } from '../../../style-settings/store/style-settings-store';
9
+ import { availableButtonStyleConfigs } from '@brand-page-maker/models';
10
+ import type { ContentPage } from '../../models';
11
+
12
+ // Explicit stub: the auto-stub misses mixin-declared props, so props('value') would be undefined.
13
+ const TextStyleFieldStub = {
14
+ name: 'TextStyleField',
15
+ props: ['value'],
16
+ template: '<div />',
17
+ };
18
+
19
+ const UrlFieldStub = {
20
+ name: 'UrlField',
21
+ props: ['settings'],
22
+ template: '<div />',
23
+ };
24
+
25
+ const TextFieldStub = {
26
+ name: 'TextField',
27
+ // showErrorWhenEmpty typed so the bare attribute casts to true, as on the real component.
28
+ props: { settings: Object, showErrorWhenEmpty: Boolean },
29
+ template: '<div />',
30
+ };
31
+
32
+ // Fields are found by data-test hook (two of them are ColorFields).
33
+ const emitFrom = (
34
+ wrapper: VueWrapper,
35
+ hook: string,
36
+ payload: unknown,
37
+ ): void => {
38
+ (wrapper.findComponent(hook) as unknown as VueWrapper).vm.$emit(
39
+ 'update:modelValue',
40
+ payload,
41
+ );
42
+ };
43
+
44
+ // Every edit leaves through one on-change patch (the mixin writes it to both viewports).
45
+ describe('HeaderButtonSettings', () => {
46
+ beforeEach(() => {
47
+ setActivePinia(createPinia());
48
+ });
49
+
50
+ const mountPanel = (value?: HeaderButton) =>
51
+ shallowMount(HeaderButtonSettings, {
52
+ props: {
53
+ value,
54
+ isAdminMaker: false,
55
+ logoTheme: {} as LogoTheme,
56
+ },
57
+ global: {
58
+ stubs: {
59
+ TextStyleField: TextStyleFieldStub,
60
+ UrlField: UrlFieldStub,
61
+ TextField: TextFieldStub,
62
+ },
63
+ },
64
+ });
65
+
66
+ const enabled: HeaderButton = {
67
+ isVisible: true,
68
+ text: '',
69
+ buttonLinkType: 'url',
70
+ };
71
+
72
+ const fieldHooks = [
73
+ '[data-test-header-button-text]',
74
+ '[data-test-header-button-link-type]',
75
+ '[data-test-header-button-text-style]',
76
+ '[data-test-header-button-fill-color]',
77
+ ];
78
+
79
+ // The show/hide toggle lives in HeaderSettingsPanel, so every field here renders unconditionally.
80
+ test('every field renders', () => {
81
+ const wrapper = mountPanel(enabled);
82
+
83
+ fieldHooks.forEach((hook) => {
84
+ expect(wrapper.find(hook).exists()).toBe(true);
85
+ });
86
+ });
87
+
88
+ // Border color only shows when the selected button style draws a border.
89
+ test('the border colour field is hidden while the selected button style draws no border', () => {
90
+ // default selectedButtonStyleId is BUTTON_STYLE_1, whose options carry no border
91
+ const wrapper = mountPanel(enabled);
92
+
93
+ expect(
94
+ wrapper.find('[data-test-header-button-border-color]').exists(),
95
+ ).toBe(false);
96
+ });
97
+
98
+ test('the border colour field renders when the selected button style has a border', () => {
99
+ useUiStyleSettingsStore().$patch(($state) => {
100
+ // availableButtonStyleConfigs starts empty until the store initialises
101
+ $state.availableButtonStyleConfigs = availableButtonStyleConfigs;
102
+ $state.buttonStyles.selectedButtonStyleId = 'BUTTON_STYLE_4';
103
+ });
104
+
105
+ const wrapper = mountPanel(enabled);
106
+
107
+ expect(
108
+ wrapper.find('[data-test-header-button-border-color]').exists(),
109
+ ).toBe(true);
110
+ });
111
+
112
+ test('the link-value field follows the selected link kind', () => {
113
+ const kinds = {
114
+ url: '[data-test-header-button-url]',
115
+ page: '[data-test-header-button-page]',
116
+ mailTo: '[data-test-header-button-email]',
117
+ tel: '[data-test-header-button-tel]',
118
+ } as const;
119
+
120
+ Object.entries(kinds).forEach(([kind, hook]) => {
121
+ const wrapper = mountPanel({
122
+ ...enabled,
123
+ buttonLinkType: kind as HeaderButton['buttonLinkType'],
124
+ });
125
+
126
+ Object.values(kinds).forEach((otherHook) => {
127
+ expect(wrapper.find(otherHook).exists()).toBe(otherHook === hook);
128
+ });
129
+ });
130
+ });
131
+
132
+ const linkTypeValues = (wrapper: VueWrapper): string[] => {
133
+ const group = wrapper.findComponent(
134
+ '[data-test-header-button-link-type]',
135
+ ) as unknown as { props(name: string): unknown };
136
+
137
+ return (group.props('options') as { value: string }[]).map(
138
+ (option) => option.value,
139
+ );
140
+ };
141
+
142
+ test('the four link kinds are offered in a 2-column group', () => {
143
+ const wrapper = mountPanel(enabled);
144
+
145
+ expect(linkTypeValues(wrapper)).toEqual(['url', 'page', 'mailTo', 'tel']);
146
+ expect(
147
+ wrapper.find('[data-test-header-button-link-type]').attributes('columns'),
148
+ ).toBe('2');
149
+ });
150
+
151
+ test('a single-page site drops the Page kind (buttonLinkStyleConvertor)', () => {
152
+ useUiPageSettingsStore().setPages({
153
+ pages: [{ id: 'page-1', title: 'Home', contents: [] }] as ContentPage[],
154
+ });
155
+
156
+ expect(linkTypeValues(mountPanel(enabled))).toEqual([
157
+ 'url',
158
+ 'mailTo',
159
+ 'tel',
160
+ ]);
161
+ });
162
+
163
+ test('a label edit emits only the text patch', () => {
164
+ const wrapper = mountPanel(enabled);
165
+
166
+ emitFrom(wrapper, '[data-test-header-button-text]', {
167
+ id: 'headerButton',
168
+ name: 'text',
169
+ value: 'Book now',
170
+ });
171
+
172
+ expect(wrapper.emitted('on-change')).toEqual([[{ text: 'Book now' }]]);
173
+ });
174
+
175
+ // Unset text style must show the renderer's Buttons default, not None/blank.
176
+ test('an unset text style presents the Buttons-linked default', () => {
177
+ const wrapper = mountPanel(enabled);
178
+
179
+ expect(
180
+ wrapper.findComponent({ name: 'TextStyleField' }).props('value'),
181
+ ).toEqual({
182
+ textStyleType: 'buttons',
183
+ });
184
+ });
185
+
186
+ test('an authored text style is passed through untouched', () => {
187
+ const wrapper = mountPanel({
188
+ ...enabled,
189
+ buttonTextStyle: { fontFamily: 'Lora', textAlign: 'center' },
190
+ });
191
+
192
+ expect(
193
+ wrapper.findComponent({ name: 'TextStyleField' }).props('value'),
194
+ ).toEqual({
195
+ fontFamily: 'Lora',
196
+ textAlign: 'center',
197
+ });
198
+ });
199
+
200
+ // Stricter than the banner: the link is always required, whatever the label holds.
201
+ test('a labelled button with an empty link shows the error', () => {
202
+ const wrapper = mountPanel({ ...enabled, text: 'Contact Us' });
203
+
204
+ expect(
205
+ wrapper.findComponent({ name: 'UrlField' }).props('settings'),
206
+ ).toMatchObject({ isValid: false });
207
+ });
208
+
209
+ test('an unlabelled button with an empty link also shows the error', () => {
210
+ const wrapper = mountPanel(enabled);
211
+
212
+ expect(
213
+ wrapper.findComponent({ name: 'UrlField' }).props('settings'),
214
+ ).toMatchObject({ isValid: false });
215
+ });
216
+
217
+ test('a valid link is accepted while the label is still empty', () => {
218
+ const wrapper = mountPanel({ ...enabled, url: 'https://example.com' });
219
+
220
+ expect(
221
+ wrapper.findComponent({ name: 'UrlField' }).props('settings'),
222
+ ).toMatchObject({ isValid: true });
223
+ });
224
+
225
+ test('switching the link kind clears the previous value', () => {
226
+ const wrapper = mountPanel({ ...enabled, url: 'https://example.com' });
227
+
228
+ (
229
+ wrapper.findComponent(
230
+ '[data-test-header-button-link-type]',
231
+ ) as unknown as VueWrapper
232
+ ).vm.$emit('on-select', { value: 'mailTo' });
233
+
234
+ expect(wrapper.emitted('on-change')).toEqual([
235
+ [{ buttonLinkType: 'mailTo', url: '' }],
236
+ ]);
237
+ });
238
+
239
+ // The label is required — an empty one hides the button on the site.
240
+ test.each([[''], [' ']])(
241
+ 'a blank label shows the required error (%j)',
242
+ (text) => {
243
+ const wrapper = mountPanel({ ...enabled, text });
244
+
245
+ expect(
246
+ wrapper.findComponent({ name: 'TextField' }).props('settings'),
247
+ ).toMatchObject({ isValid: false });
248
+ },
249
+ );
250
+
251
+ test('the label shows its error as soon as it is cleared', () => {
252
+ const wrapper = mountPanel(enabled);
253
+
254
+ expect(
255
+ wrapper.findComponent({ name: 'TextField' }).props('showErrorWhenEmpty'),
256
+ ).toBe(true);
257
+ });
258
+
259
+ test('a filled label is valid', () => {
260
+ const wrapper = mountPanel({ ...enabled, text: 'Contact Us' });
261
+
262
+ expect(
263
+ wrapper.findComponent({ name: 'TextField' }).props('settings'),
264
+ ).toMatchObject({ isValid: true });
265
+ });
266
+
267
+ // A transparent fill or border would leave a ghost button, so neither offers the option.
268
+ test.each([
269
+ ['fill', '[data-test-header-button-fill-color]', 'BUTTON_STYLE_1'],
270
+ ['border', '[data-test-header-button-border-color]', 'BUTTON_STYLE_4'],
271
+ ])(
272
+ 'the %s colour field does not offer transparent',
273
+ (_name, hook, styleId) => {
274
+ useUiStyleSettingsStore().$patch(($state) => {
275
+ $state.availableButtonStyleConfigs = availableButtonStyleConfigs;
276
+ // BUTTON_STYLE_4 draws a border, so the border field renders.
277
+ $state.buttonStyles.selectedButtonStyleId = styleId;
278
+ });
279
+
280
+ const wrapper = mountPanel(enabled);
281
+
282
+ expect(
283
+ (
284
+ wrapper.findComponent(hook) as unknown as {
285
+ props(name: string): unknown;
286
+ }
287
+ ).props('disableTransparent'),
288
+ ).toBe(true);
289
+ },
290
+ );
291
+
292
+ test('a fill-colour pick emits only the fillColor patch', () => {
293
+ const wrapper = mountPanel(enabled);
294
+
295
+ emitFrom(wrapper, '[data-test-header-button-fill-color]', {
296
+ id: 'fillColor',
297
+ name: 'fillColor',
298
+ value: 'var(--palette-2)',
299
+ });
300
+
301
+ expect(wrapper.emitted('on-change')).toEqual([
302
+ [{ fillColor: 'var(--palette-2)' }],
303
+ ]);
304
+ });
305
+ });
@@ -0,0 +1,265 @@
1
+ <template>
2
+ <div data-component="header-button-settings">
3
+ <p class="header-button-settings_option-title tw-mb-2">
4
+ {{ tr('pageSettingsSection.buttonLabel') }}
5
+ </p>
6
+ <TextField
7
+ id="headerButton"
8
+ data-test-header-button-text
9
+ name="text"
10
+ show-error-when-empty
11
+ :value="text"
12
+ :placeholder="tr('pageSettingsSection.buttonLabelPlaceholder')"
13
+ :max-length="BUTTON_TEXT_MAX_LENGTH"
14
+ :settings="labelSettings"
15
+ @update:model-value="onTextChange"
16
+ />
17
+
18
+ <p class="header-button-settings_option-title tw-mt-4 tw-mb-2">
19
+ {{ tr('pageSettingsSection.buttonLink') }}
20
+ </p>
21
+ <ButtonGroup
22
+ data-test-header-button-link-type
23
+ :options="linkTypeOptions"
24
+ :columns="linkTypeColumns"
25
+ @on-select="onSelectLinkType"
26
+ />
27
+
28
+ <div class="tw-mt-2">
29
+ <PageDropdownField
30
+ v-if="buttonLinkType === BUTTON_LINK_TYPE.page"
31
+ id="headerButton"
32
+ data-test-header-button-page
33
+ name="buttonPageId"
34
+ :value="buttonPageId"
35
+ :placeholder="contentSettingsTr('selectAPagePlaceholder')"
36
+ @update:model-value="onPageChange"
37
+ />
38
+ <EmailField
39
+ v-else-if="buttonLinkType === BUTTON_LINK_TYPE.mailTo"
40
+ id="headerButton"
41
+ data-test-header-button-email
42
+ name="url"
43
+ role="email"
44
+ :value="url"
45
+ :placeholder="contentSettingsTr('emailPlaceholder')"
46
+ :settings="linkValueSettings"
47
+ @update:model-value="onUrlChange"
48
+ />
49
+ <TelField
50
+ v-else-if="buttonLinkType === BUTTON_LINK_TYPE.tel"
51
+ id="headerButton"
52
+ data-test-header-button-tel
53
+ name="url"
54
+ role="tel"
55
+ :value="url"
56
+ :placeholder="TEL_PLACEHOLDER"
57
+ :settings="linkValueSettings"
58
+ @update:model-value="onUrlChange"
59
+ />
60
+ <UrlField
61
+ v-else
62
+ id="headerButton"
63
+ data-test-header-button-url
64
+ name="url"
65
+ role="url"
66
+ :value="url"
67
+ :placeholder="contentSettingsTr('exampleWebsitePlaceholder')"
68
+ :settings="linkValueSettings"
69
+ @update:model-value="onUrlChange"
70
+ />
71
+ </div>
72
+
73
+ <TextStyleField
74
+ id="header-button-text-style"
75
+ data-test-header-button-text-style
76
+ name="buttonTextStyle"
77
+ :label="contentSettingsTr('buttonTextStyleLabel')"
78
+ :is-admin-maker="isAdminMaker"
79
+ :logo-theme="logoTheme"
80
+ :value="buttonTextStyle"
81
+ :show-text-alignment-options="false"
82
+ @update:model-value="onTextStyleChange"
83
+ />
84
+
85
+ <ColorField
86
+ id="fillColor"
87
+ data-test-header-button-fill-color
88
+ name="fillColor"
89
+ :label="tr('pageSettingsSection.buttonFillColor')"
90
+ :value="fillColor"
91
+ :is-admin-maker="isAdminMaker"
92
+ :logo-theme="logoTheme"
93
+ :disable-transparent="true"
94
+ @update:model-value="onFillColorChange"
95
+ />
96
+
97
+ <ColorField
98
+ v-if="supportsBorderColor"
99
+ id="borderColor"
100
+ data-test-header-button-border-color
101
+ name="borderColor"
102
+ :label="tr('pageSettingsSection.buttonBorderColor')"
103
+ :value="borderColor"
104
+ :is-admin-maker="isAdminMaker"
105
+ :logo-theme="logoTheme"
106
+ :disable-transparent="true"
107
+ @update:model-value="onBorderColorChange"
108
+ />
109
+ </div>
110
+ </template>
111
+ <script setup lang="ts">
112
+ import { computed } from 'vue';
113
+ import { ButtonGroup } from '@designcrowd/fe-shared-lib';
114
+ import type {
115
+ BrandPageContext,
116
+ HeaderButton,
117
+ LogoTheme,
118
+ TextStyleDeclaration,
119
+ } from '@brand-page';
120
+ import { BUTTON_LINK_TYPE, TEXT_STYLE_TYPES } from '@brand-page';
121
+ import type { ButtonLinkType } from '@brand-page';
122
+ import type { ButtonGroupOption } from '@brand-page-maker/models/button-group.model';
123
+ import { buttonLinkStyleConvertor } from '@brand-page-maker/convertors';
124
+ import { buttonLinkValidator } from '@brand-page-maker/validators';
125
+ import { useUiPageSettingsStore } from '../../store';
126
+ import { useUiStyleSettingsStore } from '../../../style-settings/store/style-settings-store';
127
+ import type { ContentItemFieldSettings } from '../../../content-settings/models';
128
+ import ColorField from '../../../content-settings/components/color-field/ColorField.vue';
129
+ import EmailField from '../../../content-settings/components/email-field/EmailField.vue';
130
+ import PageDropdownField from '../../../content-settings/components/page-dropdown-field/PageDropdownField.vue';
131
+ import TelField from '../../../content-settings/components/tel-field/TelField.vue';
132
+ import TextField from '../../../content-settings/components/text-field/TextField.vue';
133
+ import TextStyleField from '../../../content-settings/components/text-style-field/TextStyleField.vue';
134
+ import UrlField from '../../../content-settings/components/url-field/UrlField.vue';
135
+ import {
136
+ contentSettingsTr,
137
+ tr,
138
+ validatorTr,
139
+ } from '@src/composables/useTranslate';
140
+
141
+ /** Same label length cap as the section buttons. */
142
+ const BUTTON_TEXT_MAX_LENGTH = 60;
143
+
144
+ /** Format example, not translated — same as the banner's tel placeholder. */
145
+ const TEL_PLACEHOLDER = '12345-6789';
146
+
147
+ interface Props {
148
+ value?: HeaderButton;
149
+ isAdminMaker: boolean;
150
+ logoTheme: LogoTheme;
151
+ }
152
+
153
+ const props = defineProps<Props>();
154
+
155
+ const pageSettingsStore = useUiPageSettingsStore();
156
+ const styleSettingsStore = useUiStyleSettingsStore();
157
+
158
+ // Only show Border color when the site's button style actually draws a border.
159
+ const supportsBorderColor = computed(
160
+ (): boolean =>
161
+ styleSettingsStore.selectedButtonStyleConfig?.options.includes(
162
+ 'buttonBorderColor',
163
+ ) === true,
164
+ );
165
+
166
+ const emit = defineEmits<{
167
+ (e: 'on-change', patch: Partial<HeaderButton>): void;
168
+ }>();
169
+
170
+ const text = computed((): string => props.value?.text || '');
171
+ // Validation matches the renderer, which trims: a whitespace-only label hides the button.
172
+ const trimmedLabel = computed((): string => text.value.trim());
173
+ const url = computed((): string => props.value?.url || '');
174
+ const buttonPageId = computed((): string => props.value?.buttonPageId || '');
175
+ const fillColor = computed((): string => props.value?.fillColor || '');
176
+ const borderColor = computed((): string => props.value?.borderColor || '');
177
+ const buttonTextStyle = computed(
178
+ // When unset, show the same Buttons-style default the site actually renders with.
179
+ (): TextStyleDeclaration =>
180
+ props.value?.buttonTextStyle ?? { textStyleType: TEXT_STYLE_TYPES.buttons },
181
+ );
182
+ const buttonLinkType = computed(
183
+ (): ButtonLinkType => props.value?.buttonLinkType || BUTTON_LINK_TYPE.url,
184
+ );
185
+
186
+ const validFieldSettings: ContentItemFieldSettings = {
187
+ errorMessage: '',
188
+ isValid: true,
189
+ };
190
+
191
+ // The label is required — a button with no text is hidden on the site.
192
+ const labelSettings = computed(
193
+ (): ContentItemFieldSettings =>
194
+ trimmedLabel.value
195
+ ? validFieldSettings
196
+ : { isValid: false, errorMessage: validatorTr('emptyStringMessage') },
197
+ );
198
+
199
+ // The link is always required, unlike the banner's (which is optional without button text).
200
+ const linkValueSettings = computed((): ContentItemFieldSettings => {
201
+ const { isValid, message } = buttonLinkValidator({
202
+ buttonLinkType: buttonLinkType.value,
203
+ url: url.value.trim(),
204
+ });
205
+
206
+ return { isValid, errorMessage: message };
207
+ });
208
+
209
+ const linkTypeConfig = computed(() =>
210
+ buttonLinkStyleConvertor.convert(
211
+ buttonLinkType.value,
212
+ // Only `pages` is read (drops the Page option on single-page sites).
213
+ { pages: pageSettingsStore.pages } as unknown as BrandPageContext,
214
+ ),
215
+ );
216
+
217
+ const linkTypeOptions = computed(
218
+ (): ButtonGroupOption[] => linkTypeConfig.value.options,
219
+ );
220
+
221
+ const linkTypeColumns = computed(
222
+ (): number | undefined => linkTypeConfig.value.columns,
223
+ );
224
+
225
+ const onTextChange = ({ value }: { value: string }): void => {
226
+ emit('on-change', { text: value });
227
+ };
228
+
229
+ // Switching the link kind clears the old value — a url is not a valid email/phone (same as the banner).
230
+ const onSelectLinkType = (option: ButtonGroupOption): void => {
231
+ emit('on-change', {
232
+ buttonLinkType: option.value as ButtonLinkType,
233
+ url: '',
234
+ });
235
+ };
236
+
237
+ const onUrlChange = ({ value }: { value: string }): void => {
238
+ emit('on-change', { url: value });
239
+ };
240
+
241
+ const onPageChange = ({ value }: { value: string }): void => {
242
+ emit('on-change', { buttonPageId: value });
243
+ };
244
+
245
+ const onTextStyleChange = ({
246
+ value,
247
+ }: {
248
+ value: TextStyleDeclaration;
249
+ }): void => {
250
+ emit('on-change', { buttonTextStyle: value });
251
+ };
252
+
253
+ const onFillColorChange = ({ value }: { value: string }): void => {
254
+ emit('on-change', { fillColor: value });
255
+ };
256
+
257
+ const onBorderColorChange = ({ value }: { value: string }): void => {
258
+ emit('on-change', { borderColor: value });
259
+ };
260
+ </script>
261
+ <style scoped>
262
+ .header-button-settings_option-title {
263
+ @apply tw-font-sans tw-font-bold tw-text-secondary-800 tw-text-sm;
264
+ }
265
+ </style>