@designcrowd/library.brand-page 6.2.0 → 6.2.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 (121) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-VQCQvlMg.js → CreatedOnBrandCrowd-de-de-CtvPtYF6.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-DSf3BjCG.js → CreatedOnBrandCrowd-en-us-eGF-EjAX.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DGcdM9Ec.js → CreatedOnBrandCrowd-es-es-CSemsOtT.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-C9g1fc39.js → CreatedOnBrandCrowd-fr-ca-DzylX-AT.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-C9g1fc39.js → CreatedOnBrandCrowd-fr-fr-DzylX-AT.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-DSXr7Bxl.js → CreatedOnBrandCrowd-pt-br-QMUxXQsE.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-DSXr7Bxl.js → CreatedOnBrandCrowd-pt-pt-QMUxXQsE.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-BPdCL6wS.js → CreatedOnDcom-de-de-M7OoJt-1.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-CLkSTQRu.js → CreatedOnDcom-en-us-D7hMM0AI.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-CRknMAGv.js → CreatedOnDcom-es-es-CLw-BLp2.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-ClAY_dHI.js → CreatedOnDcom-fr-ca-BRu_kmeu.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DQtv9rR-.js → CreatedOnDcom-fr-fr-IMnEv_yM.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-BbIxvmxv.js → CreatedOnDcom-pt-br-Bt6v1p27.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-BOfOesBj.js → CreatedOnDcom-pt-pt-BVsFJIJg.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/index.d.ts +12 -0
  26. package/lib/src/brand-page/models/index.js +4 -0
  27. package/lib/src/brand-page/utils/context.util.d.ts +1 -0
  28. package/lib/src/brand-page/utils/context.util.js +9 -0
  29. package/lib/src/brand-page/utils/route.util.d.ts +1 -0
  30. package/lib/src/brand-page/utils/route.util.js +9 -0
  31. package/lib/src/brand-page/utils/view-model.util.js +53 -1
  32. package/lib/src/brand-page/view-models.d.ts +3 -1
  33. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +2 -1
  34. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +2 -1
  35. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +1626 -0
  36. package/lib/src/brand-page-maker/validators/section-button.validator.d.ts +5 -2
  37. package/lib/src/brand-page-maker/validators/section-button.validator.js +11 -15
  38. package/lib/src/{bundled-translations-B2q5GWTQ.js → bundled-translations-KY0aSmMp.js} +9 -9
  39. package/lib/src/{bundled-translations.de-DE-FhH2H_rd.js → bundled-translations.de-DE-BIFi4bYj.js} +9 -9
  40. package/lib/src/{bundled-translations.es-ES-_ZExxMi7.js → bundled-translations.es-ES-CHlKuPLO.js} +5 -5
  41. package/lib/src/{bundled-translations.fr-CA-Tg-ODIiC.js → bundled-translations.fr-CA-DR1fvm8e.js} +1 -1
  42. package/lib/src/{bundled-translations.fr-FR-C0sU51Mz.js → bundled-translations.fr-FR-DnpWVIQg.js} +9 -9
  43. package/lib/src/{bundled-translations.pt-BR-MYFIhA4u.js → bundled-translations.pt-BR-p8szTOiT.js} +17 -17
  44. package/lib/src/{bundled-translations.pt-PT-DeUBSzCB.js → bundled-translations.pt-PT-DEk39NhW.js} +22 -22
  45. package/lib/src/bundles/bundled-translations.de-DE.json +6 -1
  46. package/lib/src/bundles/bundled-translations.es-ES.json +6 -1
  47. package/lib/src/bundles/bundled-translations.fr-CA.json +6 -1
  48. package/lib/src/bundles/bundled-translations.fr-FR.json +6 -1
  49. package/lib/src/bundles/bundled-translations.json +6 -1
  50. package/lib/src/bundles/bundled-translations.pt-BR.json +6 -1
  51. package/lib/src/bundles/bundled-translations.pt-PT.json +6 -1
  52. package/lib/src/css/library.brand-page-brandCrowd.min.css +573 -135
  53. package/lib/src/css/library.brand-page-brandPage.min.css +573 -135
  54. package/lib/src/css/library.brand-page-designCom.min.css +573 -135
  55. package/lib/src/css/library.brand-page-preview.min.css +573 -135
  56. package/lib/src/index.mjs +1 -1
  57. package/lib/src/{lib-BMVF4uNy.js → lib-B38VVCSw.js} +20304 -19843
  58. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +2 -1
  59. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
  60. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
  61. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +1 -1
  62. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +14 -2
  63. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.js +14 -0
  64. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
  65. package/lib/src/shared/page-navigation/store/page-navigation-store.d.ts +1 -0
  66. package/lib/src/shared/page-navigation/store/page-navigation-store.js +12 -0
  67. package/lib/src/shared/page-settings/components/delete-page-modal/DeletePageModal.mixin.d.ts +1 -1
  68. package/lib/src/shared/page-settings/components/new-page-modal/NewPageModal.mixin.d.ts +2 -1
  69. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +190 -1
  70. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +184 -2
  71. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +34 -2
  72. package/lib/src/shared/page-settings/store/page-settings-store.d.ts +2 -2
  73. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +3 -0
  74. package/package.json +2 -2
  75. package/src/brand-page/components/navigation/hamburger/HamburgerNavigation.vue +1 -0
  76. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts +18 -1
  77. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue +73 -3
  78. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue.test.ts +181 -0
  79. package/src/brand-page/components/page/Page.mixin.ts +21 -11
  80. package/src/brand-page/components/page/Page.vue +2 -0
  81. package/src/brand-page/components/page-header/HeaderLayout.stories.ts +10 -0
  82. package/src/brand-page/components/page-header/HeaderLayout.vue +415 -71
  83. package/src/brand-page/components/page-header/PageHeader.mixin.ts +14 -10
  84. package/src/brand-page/components/page-header/PageHeader.vue +16 -1
  85. package/src/brand-page/components/page-header/PageHeader.vue.test.ts +339 -0
  86. package/src/brand-page/constants/sections/header-display.constants.test.ts +44 -0
  87. package/src/brand-page/constants/sections/header-display.constants.ts +35 -0
  88. package/src/brand-page/models/header-button.model.test.ts +85 -0
  89. package/src/brand-page/models/index.ts +19 -0
  90. package/src/brand-page/utils/context.util.ts +15 -0
  91. package/src/brand-page/utils/route.util.ts +13 -0
  92. package/src/brand-page/utils/view-model.util.test.ts +151 -1
  93. package/src/brand-page/utils/view-model.util.ts +73 -1
  94. package/src/brand-page/view-models.ts +3 -0
  95. package/src/brand-page-maker/validators/section-button.validator.ts +23 -19
  96. package/src/bundles/bundled-translations.de-DE.json +6 -1
  97. package/src/bundles/bundled-translations.es-ES.json +6 -1
  98. package/src/bundles/bundled-translations.fr-CA.json +6 -1
  99. package/src/bundles/bundled-translations.fr-FR.json +6 -1
  100. package/src/bundles/bundled-translations.json +6 -1
  101. package/src/bundles/bundled-translations.pt-BR.json +6 -1
  102. package/src/bundles/bundled-translations.pt-PT.json +6 -1
  103. package/src/shared/content-settings/components/text-field/TextField.mixin.ts +15 -0
  104. package/src/shared/content-settings/components/url-field/UrlField.vue +1 -0
  105. package/src/shared/page-navigation/store/page-navigation-store.ts +16 -0
  106. package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.button-section.test.ts +131 -0
  107. package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.vue +65 -10
  108. package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.test.ts +305 -0
  109. package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.vue +265 -0
  110. package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.test.ts +354 -0
  111. package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.vue +48 -9
  112. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.header-button.test.ts +114 -0
  113. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +38 -1
  114. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.de-DE.json +6 -1
  115. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.es-ES.json +6 -1
  116. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.fr-CA.json +6 -1
  117. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.fr-FR.json +6 -1
  118. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.json +6 -1
  119. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.pt-BR.json +6 -1
  120. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.pt-PT.json +6 -1
  121. package/src/shared/page-settings/store/page-settings-store.ts +2 -0
@@ -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>
@@ -0,0 +1,354 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest';
2
+ import { createPinia, setActivePinia } from 'pinia';
3
+ import { shallowMount } from '@vue/test-utils';
4
+ import { ButtonGroup } from '@designcrowd/fe-shared-lib';
5
+ import {
6
+ HEADER_DISPLAY,
7
+ HEADER_DISPLAY_STYLE_CONFIGS,
8
+ NAVIGATION_DISPLAY,
9
+ VIEWPORTS,
10
+ } from '@brand-page';
11
+ import type { ViewportsType } from '@brand-page';
12
+ import type { ButtonGroupOption } from '@brand-page-maker/models/button-group.model';
13
+ import { usePageNavigationStore } from '../../../page-navigation/store/page-navigation-store';
14
+ import HeaderDisplayStyle from './HeaderDisplayStyle.vue';
15
+
16
+ // A visible nav needs more than the home page (mirrors route.util.ts hasVisibleNavigation).
17
+ const seedNavPages = (count: number): void => {
18
+ const store = usePageNavigationStore();
19
+ const routes = Array.from({ length: count }, (_, i) => ({
20
+ id: `route-${i}`,
21
+ pageId: `page-${i}`,
22
+ pageSlug: `page-${i}`,
23
+ isHidden: false,
24
+ displayName: `Page ${i}`,
25
+ }));
26
+
27
+ store.routes = routes;
28
+ store.orders = routes.map((route) => ({
29
+ routeId: route.id,
30
+ subRoutes: [],
31
+ }));
32
+ };
33
+
34
+ // The button swaps bar + desktop-hamburger tiles to with-button artwork; mobile hamburger stays plain (button lives in the side menu), dataAttribute never changes.
35
+ describe('HeaderDisplayStyle — header-button icon swap', () => {
36
+ beforeEach(() => {
37
+ setActivePinia(createPinia());
38
+ seedNavPages(3);
39
+ });
40
+
41
+ const mountTiles = ({
42
+ displayStyle,
43
+ navigationDisplayType,
44
+ viewport = VIEWPORTS.desktop as ViewportsType,
45
+ isLogoVisible = true,
46
+ showHeaderButton = false,
47
+ }: {
48
+ displayStyle: string;
49
+ navigationDisplayType: string;
50
+ viewport?: ViewportsType;
51
+ isLogoVisible?: boolean;
52
+ showHeaderButton?: boolean;
53
+ }): ButtonGroupOption[] => {
54
+ const wrapper = shallowMount(HeaderDisplayStyle, {
55
+ props: {
56
+ displayStyle,
57
+ navigationDisplayType,
58
+ viewport,
59
+ isLogoVisible,
60
+ showHeaderButton,
61
+ onSelect: () => undefined,
62
+ },
63
+ });
64
+
65
+ return wrapper
66
+ .findComponent(ButtonGroup)
67
+ .props('options') as ButtonGroupOption[];
68
+ };
69
+
70
+ const desktopBar = (showHeaderButton: boolean) =>
71
+ mountTiles({
72
+ displayStyle: HEADER_DISPLAY.DESKTOP_TAB_1,
73
+ navigationDisplayType: NAVIGATION_DISPLAY.TAB_1,
74
+ showHeaderButton,
75
+ });
76
+
77
+ const iconNames = (options: ButtonGroupOption[]) =>
78
+ options.map((option) => ('iconName' in option ? option.iconName : ''));
79
+
80
+ test('off: the six desktop bar tiles keep their plain icons', () => {
81
+ expect(iconNames(desktopBar(false))).toEqual([
82
+ 'header-desktop-nav-style-tab-display-style-1',
83
+ 'header-desktop-nav-style-tab-display-style-2',
84
+ 'header-desktop-nav-style-tab-display-style-3',
85
+ 'header-desktop-nav-style-tab-display-style-4',
86
+ 'header-desktop-nav-style-tab-display-style-5',
87
+ 'header-desktop-nav-style-tab-display-style-6',
88
+ ]);
89
+ });
90
+
91
+ test('on: the six desktop bar tiles swap to the with-button icons', () => {
92
+ expect(iconNames(desktopBar(true))).toEqual([
93
+ 'header-desktop-nav-style-tab-display-style-with-button-1',
94
+ 'header-desktop-nav-style-tab-display-style-with-button-2',
95
+ 'header-desktop-nav-style-tab-display-style-with-button-3',
96
+ 'header-desktop-nav-style-tab-display-style-with-button-4',
97
+ 'header-desktop-nav-style-tab-display-style-with-button-5',
98
+ 'header-desktop-nav-style-tab-display-style-with-button-6',
99
+ ]);
100
+ });
101
+
102
+ test('value, dataAttribute and iconViewBox survive the swap', () => {
103
+ const off = desktopBar(false);
104
+ const on = desktopBar(true);
105
+
106
+ expect(on.map((option) => option.value)).toEqual(
107
+ off.map((option) => option.value),
108
+ );
109
+ expect(on.map((option) => option.dataAttribute)).toEqual(
110
+ off.map((option) => option.dataAttribute),
111
+ );
112
+ expect(
113
+ on.map((option) => ('iconViewBox' in option ? option.iconViewBox : '')),
114
+ ).toEqual([
115
+ '0 0 160 56',
116
+ '0 0 160 56',
117
+ '0 0 160 56',
118
+ '0 0 160 56',
119
+ '0 0 160 56',
120
+ '0 0 160 56',
121
+ ]);
122
+ });
123
+
124
+ test('on: the mobile bar tiles swap to the with-button icons', () => {
125
+ const options = mountTiles({
126
+ displayStyle: HEADER_DISPLAY.MOBILE_TAB_1,
127
+ navigationDisplayType: NAVIGATION_DISPLAY.TAB_1,
128
+ viewport: VIEWPORTS.mobile,
129
+ showHeaderButton: true,
130
+ });
131
+
132
+ expect(iconNames(options)).toEqual([
133
+ 'header-mobile-nav-style-tab-display-style-with-button-1',
134
+ 'header-mobile-nav-style-tab-display-style-with-button-2',
135
+ ]);
136
+ });
137
+
138
+ test('on: the desktop hamburger tiles swap to the with-button icons', () => {
139
+ const options = mountTiles({
140
+ displayStyle: HEADER_DISPLAY.DESKTOP_HAMBURGER_1,
141
+ navigationDisplayType: NAVIGATION_DISPLAY.HAMBURGER_1,
142
+ showHeaderButton: true,
143
+ });
144
+
145
+ expect(iconNames(options)).toEqual([
146
+ 'header-desktop-nav-style-hamburger-display-style-with-button-1',
147
+ 'header-desktop-nav-style-hamburger-display-style-with-button-2',
148
+ ]);
149
+ });
150
+
151
+ // Mobile hamburger has no with-button artwork on purpose: the button lives in the side menu.
152
+ test('the mobile hamburger tiles are left alone', () => {
153
+ const options = mountTiles({
154
+ displayStyle: HEADER_DISPLAY.MOBILE_HAMBURGER_1,
155
+ navigationDisplayType: NAVIGATION_DISPLAY.HAMBURGER_1,
156
+ viewport: 'mobile',
157
+ showHeaderButton: true,
158
+ });
159
+
160
+ expect(
161
+ iconNames(options).every((name) => !name.includes('with-button')),
162
+ ).toBe(true);
163
+ });
164
+
165
+ test('the desktop no-logo bar tiles are left alone', () => {
166
+ const options = mountTiles({
167
+ displayStyle: HEADER_DISPLAY.DESKTOP_TAB_NO_LOGO_1,
168
+ navigationDisplayType: NAVIGATION_DISPLAY.TAB_1,
169
+ isLogoVisible: false,
170
+ showHeaderButton: true,
171
+ });
172
+
173
+ expect(
174
+ iconNames(options).every((name) => !name.includes('with-button')),
175
+ ).toBe(true);
176
+ });
177
+
178
+ test('the shared display-style configs are never mutated in place', () => {
179
+ desktopBar(true);
180
+
181
+ // Absolute checks against the authored values, so any in-place write is caught.
182
+ HEADER_DISPLAY_STYLE_CONFIGS.forEach((config) => {
183
+ expect(config.option.isSelected).toBe(false);
184
+
185
+ if ('iconName' in config.option) {
186
+ expect(config.option.iconName).not.toContain('with-button');
187
+ }
188
+ });
189
+ });
190
+ });
191
+
192
+ // Desktop no-logo hamburger with the button on pins icon + button right, so the picker hides.
193
+ describe('HeaderDisplayStyle — hidden for no-logo hamburger with the button', () => {
194
+ beforeEach(() => {
195
+ setActivePinia(createPinia());
196
+ });
197
+
198
+ const mountWrapper = ({
199
+ viewport = VIEWPORTS.desktop as ViewportsType,
200
+ isLogoVisible = false,
201
+ showHeaderButton = true,
202
+ navigationDisplayType = NAVIGATION_DISPLAY.HAMBURGER_1,
203
+ } = {}) =>
204
+ shallowMount(HeaderDisplayStyle, {
205
+ props: {
206
+ displayStyle: HEADER_DISPLAY.DESKTOP_HAMBURGER_NO_LOGO_3,
207
+ navigationDisplayType,
208
+ viewport,
209
+ isLogoVisible,
210
+ showHeaderButton,
211
+ onSelect: () => undefined,
212
+ },
213
+ });
214
+
215
+ test('desktop + no logo + hamburger + button: the picker does not render', () => {
216
+ expect(mountWrapper().findComponent(ButtonGroup).exists()).toBe(false);
217
+ });
218
+
219
+ test('the picker stays with the button off', () => {
220
+ expect(
221
+ mountWrapper({ showHeaderButton: false })
222
+ .findComponent(ButtonGroup)
223
+ .exists(),
224
+ ).toBe(true);
225
+ });
226
+
227
+ test('the picker stays on mobile', () => {
228
+ expect(
229
+ mountWrapper({ viewport: VIEWPORTS.mobile as ViewportsType })
230
+ .findComponent(ButtonGroup)
231
+ .exists(),
232
+ ).toBe(true);
233
+ });
234
+ });
235
+
236
+ // Desktop no-logo bar nav with the button on and no visible pages: the nav is hidden and
237
+ // the button holds the right edge in every variant, so the picker hides too.
238
+ describe('HeaderDisplayStyle — hidden for no-logo bar nav without pages', () => {
239
+ beforeEach(() => {
240
+ setActivePinia(createPinia());
241
+ });
242
+
243
+ const mountWrapper = ({
244
+ viewport = VIEWPORTS.desktop as ViewportsType,
245
+ isLogoVisible = false,
246
+ showHeaderButton = true,
247
+ } = {}) =>
248
+ shallowMount(HeaderDisplayStyle, {
249
+ props: {
250
+ displayStyle: HEADER_DISPLAY.DESKTOP_TAB_NO_LOGO_1,
251
+ navigationDisplayType: NAVIGATION_DISPLAY.TAB_1,
252
+ viewport,
253
+ isLogoVisible,
254
+ showHeaderButton,
255
+ onSelect: () => undefined,
256
+ },
257
+ });
258
+
259
+ test('desktop + no logo + button + only the home page: the picker does not render', () => {
260
+ seedNavPages(1);
261
+
262
+ expect(mountWrapper().findComponent(ButtonGroup).exists()).toBe(false);
263
+ });
264
+
265
+ test('the picker stays once a second visible page exists', () => {
266
+ seedNavPages(2);
267
+
268
+ expect(mountWrapper().findComponent(ButtonGroup).exists()).toBe(true);
269
+ });
270
+
271
+ test('a hidden second page does not count as a visible nav', () => {
272
+ seedNavPages(2);
273
+ usePageNavigationStore().routes[1].isHidden = true;
274
+
275
+ expect(mountWrapper().findComponent(ButtonGroup).exists()).toBe(false);
276
+ });
277
+
278
+ test('the picker stays with the button off', () => {
279
+ seedNavPages(1);
280
+
281
+ expect(
282
+ mountWrapper({ showHeaderButton: false })
283
+ .findComponent(ButtonGroup)
284
+ .exists(),
285
+ ).toBe(true);
286
+ });
287
+
288
+ test('the picker stays with a logo', () => {
289
+ seedNavPages(1);
290
+
291
+ expect(
292
+ mountWrapper({ isLogoVisible: true }).findComponent(ButtonGroup).exists(),
293
+ ).toBe(true);
294
+ });
295
+
296
+ // No mobile case here: mobile bar no-logo has a single display style, so the picker
297
+ // never renders there regardless (the hamburger describe covers the mobile guard).
298
+ });
299
+
300
+ // Hiding the picker must not stop the component migrating a stored display style that no
301
+ // longer fits: a with-logo style left on a no-logo site collapses the header band and its
302
+ // pinned button hangs out of it. Building `options` is what performs that migration, so it
303
+ // has to be evaluated even while hidden.
304
+ describe('HeaderDisplayStyle — migrates an unsupported style while hidden', () => {
305
+ beforeEach(() => {
306
+ setActivePinia(createPinia());
307
+ });
308
+
309
+ const mountHidden = ({
310
+ displayStyle,
311
+ navigationDisplayType,
312
+ }: {
313
+ displayStyle: string;
314
+ navigationDisplayType: string;
315
+ }) => {
316
+ const selected: string[] = [];
317
+ const wrapper = shallowMount(HeaderDisplayStyle, {
318
+ props: {
319
+ displayStyle,
320
+ navigationDisplayType,
321
+ viewport: VIEWPORTS.desktop as ViewportsType,
322
+ isLogoVisible: false,
323
+ showHeaderButton: true,
324
+ onSelect: (value: string) => selected.push(value),
325
+ },
326
+ });
327
+
328
+ return { wrapper, selected };
329
+ };
330
+
331
+ test('bar nav without pages: the with-logo style is swapped for a no-logo one', () => {
332
+ seedNavPages(1);
333
+
334
+ const { wrapper, selected } = mountHidden({
335
+ displayStyle: HEADER_DISPLAY.DESKTOP_TAB_5,
336
+ navigationDisplayType: NAVIGATION_DISPLAY.TAB_1,
337
+ });
338
+
339
+ expect(wrapper.findComponent(ButtonGroup).exists()).toBe(false);
340
+ expect(selected).toEqual([HEADER_DISPLAY.DESKTOP_TAB_NO_LOGO_2]);
341
+ });
342
+
343
+ test('hamburger: the with-logo style is swapped for a no-logo one', () => {
344
+ seedNavPages(3);
345
+
346
+ const { wrapper, selected } = mountHidden({
347
+ displayStyle: HEADER_DISPLAY.DESKTOP_HAMBURGER_1,
348
+ navigationDisplayType: NAVIGATION_DISPLAY.HAMBURGER_1,
349
+ });
350
+
351
+ expect(wrapper.findComponent(ButtonGroup).exists()).toBe(false);
352
+ expect(selected).toEqual([HEADER_DISPLAY.DESKTOP_HAMBURGER_NO_LOGO_3]);
353
+ });
354
+ });