@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
@@ -1,7 +1,7 @@
1
1
  import { defineComponent, computed } from 'vue';
2
2
  import { PAGE_SECTION_STYLE_OPTIONS } from '@brand-page-maker/models/page-section-style.model';
3
3
  import { PAGE_SECTIONS } from '../../models';
4
- import { LOGO_SLIDER_DESKTOP_MAX, LOGO_SLIDER_DESKTOP_MIN, LOGO_SLIDER_MOBILE_MAX, LOGO_SLIDER_MOBILE_MIN, VIEWPORTS, getNavigationType, NAVIGATION_TYPE_DEFAULT_DISPLAY_TYPE, } from '@brand-page';
4
+ import { BUTTON_LINK_TYPE, LOGO_SLIDER_DESKTOP_MAX, LOGO_SLIDER_DESKTOP_MIN, LOGO_SLIDER_MOBILE_MAX, LOGO_SLIDER_MOBILE_MIN, VIEWPORTS, getNavigationType, NAVIGATION_TYPE_DEFAULT_DISPLAY_TYPE, } from '@brand-page';
5
5
  import { UI_DEVICE_MODES, UI_EVENT_SOURCES } from '../../../models';
6
6
  import { isTransparentColour } from '@brand-page/utils/color.util';
7
7
  import { useUiPageSettingsStore } from '../../store';
@@ -12,7 +12,13 @@ import { useBrandPageContainerStore, useBrandPageMakerStore, useLogoStore, useUi
12
12
  import { useUiEditableLogoStore, useUiLogoSettingsStore, } from '@shared/logo-settings/store';
13
13
  import { FabricRenderClient } from '../../../../clients/fabric-render-client';
14
14
  import { parseLogoThemeToCssStyles } from '@brand-page/utils';
15
- import { commonTr } from '@src/composables/useTranslate';
15
+ import { commonTr, pageSettingsSectionTr } from '@src/composables/useTranslate';
16
+ // Starting values for a brand-new button; real "Contact Us" text so the label is never empty.
17
+ const newHeaderButton = () => ({
18
+ isVisible: false,
19
+ text: pageSettingsSectionTr('buttonLabelPlaceholder'),
20
+ buttonLinkType: BUTTON_LINK_TYPE.url,
21
+ });
16
22
  const viewportsWithTr = () => [
17
23
  {
18
24
  key: VIEWPORTS.desktop,
@@ -100,6 +106,12 @@ export default defineComponent({
100
106
  header() {
101
107
  return this.sections[PAGE_SECTIONS.header][this.viewport];
102
108
  },
109
+ headerButton() {
110
+ return this.header.headerButton;
111
+ },
112
+ isHeaderButtonVisible() {
113
+ return this.headerButton?.isVisible === true;
114
+ },
103
115
  navigationType() {
104
116
  return getNavigationType(this.navigation.displayType);
105
117
  },
@@ -327,6 +339,26 @@ export default defineComponent({
327
339
  viewport: VIEWPORTS.desktop,
328
340
  });
329
341
  },
342
+ onHeaderButtonChange(patch) {
343
+ const value = {
344
+ ...newHeaderButton(),
345
+ ...this.headerButton,
346
+ ...patch,
347
+ };
348
+ // Write the whole button to BOTH viewports (own copy each) so they always match.
349
+ this.pageSettingsStore.setPageSettingsSectionValue({
350
+ sectionName: PAGE_SECTIONS.header,
351
+ propName: 'headerButton',
352
+ value: { ...value },
353
+ viewport: VIEWPORTS.mobile,
354
+ });
355
+ this.pageSettingsStore.setPageSettingsSectionValue({
356
+ sectionName: PAGE_SECTIONS.header,
357
+ propName: 'headerButton',
358
+ value: { ...value },
359
+ viewport: VIEWPORTS.desktop,
360
+ });
361
+ },
330
362
  onItemListIntentImageReplace(name, { index }) {
331
363
  this.contentSettingsStore.setSelectedItemAndFieldName({
332
364
  id: this.sectionName,
@@ -1,7 +1,7 @@
1
1
  import type { ContentItem } from '../../content-settings/models';
2
2
  import { PageHeaderDesktop, PageHeaderMobile, PageFooterDesktop, PageFooterMobile, VIEWPORTS } from '@brand-page';
3
3
  import type { NavigationType } from '@brand-page';
4
- import type { Content, ImageGalleryItem, LinksItem, LogoImageModel, ViewportsType, LogoTemplateImageModel, BrandPagePaymentConfig, BrandPagePaymentConfigCurrency } from '@brand-page';
4
+ import type { Content, HeaderButton, ImageGalleryItem, LinksItem, LogoImageModel, ViewportsType, LogoTemplateImageModel, BrandPagePaymentConfig, BrandPagePaymentConfigCurrency } from '@brand-page';
5
5
  import type { ContentPage } from '../models';
6
6
  import { PAGE_SECTIONS } from '../models';
7
7
  export type PageSettingsStateSectionStyles = Partial<PageHeaderDesktop & PageHeaderMobile & PageFooterDesktop & PageFooterMobile>;
@@ -74,7 +74,7 @@ export declare const useUiPageSettingsStore: import("pinia").StoreDefinition<"ui
74
74
  sectionName: PageSettingsSection;
75
75
  propName: keyof PageSettingsStateSectionStyles;
76
76
  index?: number;
77
- value: string | boolean | LinksItem[] | BrandPageCssStyle | Partial<ImageGalleryItem> | number | null | undefined;
77
+ value: string | boolean | LinksItem[] | BrandPageCssStyle | Partial<ImageGalleryItem> | HeaderButton | number | null | undefined;
78
78
  merge?: boolean;
79
79
  viewport: ViewportsType;
80
80
  }): void;
@@ -80,6 +80,7 @@ declare const _default: import("vue").DefineComponent<{}, {
80
80
  paddingTop?: string | undefined;
81
81
  position?: string | undefined;
82
82
  textAlign?: string | undefined;
83
+ textAlignLast?: string | undefined;
83
84
  textDecoration?: string | undefined;
84
85
  top?: string | undefined;
85
86
  transform?: string | undefined;
@@ -128,6 +129,7 @@ declare const _default: import("vue").DefineComponent<{}, {
128
129
  paddingTop?: string | undefined;
129
130
  position?: string | undefined;
130
131
  textAlign?: string | undefined;
132
+ textAlignLast?: string | undefined;
131
133
  textDecoration?: string | undefined;
132
134
  top?: string | undefined;
133
135
  transform?: string | undefined;
@@ -175,6 +177,7 @@ declare const _default: import("vue").DefineComponent<{}, {
175
177
  paddingTop?: string | undefined;
176
178
  position?: string | undefined;
177
179
  textAlign?: string | undefined;
180
+ textAlignLast?: string | undefined;
178
181
  textDecoration?: string | undefined;
179
182
  top?: string | undefined;
180
183
  transform?: string | undefined;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "6.2.0",
4
+ "version": "6.2.1",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -71,7 +71,7 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "@ckpack/vue-color": "1.6.0",
74
- "@designcrowd/fe-shared-lib": "2.1.1",
74
+ "@designcrowd/fe-shared-lib": "2.1.2",
75
75
  "@pinia/nuxt": "0.11.3",
76
76
  "@storybook/react": "9.0.4",
77
77
  "@tiptap/extension-bubble-menu": "3.28.0",
@@ -2,6 +2,7 @@
2
2
  <div data-component="hamburger-navigation">
3
3
  <div class="tw-w-full tw-flex tw-justify-end">
4
4
  <button
5
+ data-test-hamburger-toggle
5
6
  :style="iconStyle"
6
7
  class="tw-cursor-pointer tw-p-2"
7
8
  :class="{
@@ -6,7 +6,8 @@ import type {
6
6
  NavigationMobile,
7
7
  } from '../../../models';
8
8
  import { resolveSubNavItemBackgroundColor } from '../../../models';
9
- import type { NavItemViewModel } from '../../../view-models';
9
+ import { guardPreviewClick } from '../../../utils/context.util';
10
+ import type { ButtonViewModel, NavItemViewModel } from '../../../view-models';
10
11
 
11
12
  export default defineComponent({
12
13
  props: {
@@ -26,6 +27,13 @@ export default defineComponent({
26
27
  type: Object as PropType<NavigationDesktop | NavigationMobile>,
27
28
  required: true,
28
29
  },
30
+ // Provided only when the header row does not render the CTA (the caller
31
+ // decides via headerRowRendersCta) — absent means the side menu shows none.
32
+ headerButtonViewModel: {
33
+ type: Object as PropType<ButtonViewModel>,
34
+ required: false,
35
+ default: undefined,
36
+ },
29
37
  },
30
38
  emits: ['close-side-menu'],
31
39
  data() {
@@ -74,6 +82,9 @@ export default defineComponent({
74
82
 
75
83
  return rest;
76
84
  },
85
+ showHeaderButton(): boolean {
86
+ return Boolean(this.headerButtonViewModel);
87
+ },
77
88
  routes(): any[] {
78
89
  return this.visibleNavItems.map((navItem, _i) => {
79
90
  return {
@@ -114,6 +125,12 @@ export default defineComponent({
114
125
  closeSideMenu(): void {
115
126
  this.$emit('close-side-menu');
116
127
  },
128
+ // Preview: inert; published: follow the link and close the side menu behind it.
129
+ onSideMenuCtaClick(event: MouseEvent): void {
130
+ if (!guardPreviewClick(event, this.context)) {
131
+ this.closeSideMenu();
132
+ }
133
+ },
117
134
  toggleExpanded(pageId: string): void {
118
135
  const isCurrentlyExpanded = this.expandedItems[pageId];
119
136
 
@@ -9,15 +9,18 @@
9
9
  }"
10
10
  @click="closeSideMenu"
11
11
  />
12
+ <!-- With a button, the nav list scrolls instead of the panel so the button stays put. -->
12
13
  <div
13
14
  class="c-sidebar"
14
15
  :class="{
15
- 'c-sidebar--open tw-overflow-auto': isSideMenuOpen,
16
+ 'c-sidebar--open': isSideMenuOpen,
16
17
  'c-sidebar--closed': !isSideMenuOpen,
18
+ 'tw-overflow-auto': isSideMenuOpen && !showHeaderButton,
19
+ 'c-sidebar--has-cta': showHeaderButton,
17
20
  }"
18
21
  :style="defaultButtonStyle"
19
22
  >
20
- <div class="tw-w-full tw-flex tw-justify-end">
23
+ <div class="c-sidebar-header tw-w-full tw-flex tw-justify-end">
21
24
  <button
22
25
  class="tw-mt-4 tw-w-8 tw-h-8 tw-bg-transparent tw-border-none tw-cursor-pointer"
23
26
  :style="closeButtonStyle"
@@ -27,7 +30,7 @@
27
30
  <Icon name="close" size="sm" />
28
31
  </button>
29
32
  </div>
30
- <div class="tw-flex tw-flex-col">
33
+ <div class="c-nav-list tw-flex tw-flex-col">
31
34
  <template
32
35
  v-for="(navItem, index) in visibleNavItems"
33
36
  :key="`${navItem.relativePath}-${index}`"
@@ -91,16 +94,30 @@
91
94
  </template>
92
95
  </template>
93
96
  </div>
97
+ <div
98
+ v-if="showHeaderButton"
99
+ data-component="navigation-side-menu-cta"
100
+ class="c-sidebar-cta"
101
+ >
102
+ <Button
103
+ data-test-header-cta-button="side-menu"
104
+ :view-model="headerButtonViewModel"
105
+ :context="context"
106
+ @click.capture="onSideMenuCtaClick"
107
+ />
108
+ </div>
94
109
  </div>
95
110
  </div>
96
111
  </template>
97
112
  <script>
98
113
  import { Icon } from '@designcrowd/fe-shared-lib';
99
114
  import NavigationSideMenuMixin from './NavigationSideMenu.mixin.ts';
115
+ import Button from '../../button/Button.vue';
100
116
 
101
117
  export default {
102
118
  components: {
103
119
  Icon,
120
+ Button,
104
121
  },
105
122
  mixins: [NavigationSideMenuMixin],
106
123
  };
@@ -146,4 +163,57 @@ export default {
146
163
  .c-chevron-expanded {
147
164
  transform: rotate(180deg);
148
165
  }
166
+
167
+ /* Button pinned to the bottom of the side menu; rules only apply when the button is on. */
168
+ .c-sidebar--has-cta {
169
+ @apply tw-flex tw-flex-col;
170
+ overflow: hidden;
171
+ }
172
+
173
+ .c-sidebar--has-cta .c-sidebar-header {
174
+ flex-shrink: 0;
175
+ }
176
+
177
+ .c-sidebar--has-cta .c-nav-list {
178
+ /* Lets the link list scroll instead of the panel. */
179
+ min-height: 0;
180
+ overflow-y: auto;
181
+ }
182
+
183
+ .c-sidebar-cta {
184
+ @apply tw-relative tw-p-4;
185
+ }
186
+
187
+ /* The generic Button ships section spacing; the side menu, as the caller, strips it
188
+ (same fit the header row applies in HeaderLayout.vue). */
189
+ .c-sidebar-cta :deep(.bp-button) {
190
+ margin-bottom: 0;
191
+ min-height: 0;
192
+ }
193
+ .c-sidebar-cta :deep(.c-button__no-icon) {
194
+ display: none;
195
+ }
196
+ /* Base label class, not a specific --offset-* variant, so a future offset swap can't slide it. */
197
+ .c-sidebar-cta :deep(.c-button__label) {
198
+ transform: none;
199
+ }
200
+
201
+ .c-sidebar--has-cta .c-sidebar-cta {
202
+ /* Keeps the button at the bottom whether links underfill or overflow. */
203
+ margin-top: auto;
204
+ flex-shrink: 0;
205
+ /* Opaque so scrolled links don't show through. */
206
+ background-color: inherit;
207
+ }
208
+
209
+ /* Thin separator line above the button, faded without dimming the button itself. */
210
+ .c-sidebar-cta::before {
211
+ content: '';
212
+ position: absolute;
213
+ top: 0;
214
+ left: 0;
215
+ right: 0;
216
+ border-top: 1px solid currentColor;
217
+ opacity: 0.2;
218
+ }
149
219
  </style>
@@ -0,0 +1,181 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { createRouter, createMemoryHistory } from 'vue-router';
3
+ import { createPinia, setActivePinia } from 'pinia';
4
+ import NavigationSideMenu from './NavigationSideMenu.vue';
5
+ import {
6
+ BRAND_PAGE_TYPES,
7
+ BUTTON_LINK_TYPE,
8
+ MEDIA_SIZES,
9
+ PageHeaderMobile,
10
+ PageStyles,
11
+ type BrandPageContext,
12
+ type HeaderButton,
13
+ } from '@brand-page/models';
14
+ import type { NavItemViewModel } from '@brand-page/view-models';
15
+ import { mapToPageHeaderViewModel } from '@brand-page/utils';
16
+
17
+ const CTA = '[data-test-header-cta-button]';
18
+ const SLUG = 'my-brand';
19
+
20
+ const createContext = (): BrandPageContext =>
21
+ ({
22
+ brandPageSlug: SLUG,
23
+ brandPageToken: '',
24
+ isPreview: false,
25
+ isProduction: false,
26
+ isServerRender: false,
27
+ options: { mediaSizes: MEDIA_SIZES.sizeThumb },
28
+ isTemplate: false,
29
+ isDesignCom: false,
30
+ pageTitle: 'My Brand',
31
+ runtimeEnvironment: '',
32
+ stripePublishableKey: undefined,
33
+ recaptchaSiteKey: undefined,
34
+ customDomain: undefined,
35
+ brandPageType: BRAND_PAGE_TYPES.brandSite,
36
+ isBrandPage: false,
37
+ isBrandPageOrIsBrandContact: false,
38
+ isBrandSite: true,
39
+ isBrandContact: false,
40
+ pages: [],
41
+ isCapturingPreview: false,
42
+ currentPageRelativePath: '',
43
+ nav: {
44
+ navItems: [
45
+ {
46
+ pageId: 'home',
47
+ slug: SLUG,
48
+ displayName: 'Home',
49
+ routeName: 'home-route',
50
+ routeParams: { slug: SLUG },
51
+ isRoot: true,
52
+ isHidden: false,
53
+ relativePath: '',
54
+ nestedNavItems: [],
55
+ },
56
+ ] as NavItemViewModel[],
57
+ },
58
+ }) as BrandPageContext;
59
+
60
+ const aHeaderButton = (): HeaderButton => ({
61
+ isVisible: true,
62
+ text: 'Book now',
63
+ buttonLinkType: BUTTON_LINK_TYPE.url,
64
+ url: 'https://example.com/book',
65
+ buttonTextStyle: { textStyleType: 'buttons' },
66
+ });
67
+
68
+ const mountSideMenu = async (options: {
69
+ headerButton?: HeaderButton;
70
+ isPreview?: boolean;
71
+ }) => {
72
+ const router = createRouter({
73
+ history: createMemoryHistory(),
74
+ routes: [
75
+ {
76
+ path: '/:slug',
77
+ name: 'home-route',
78
+ component: { template: '<div />' },
79
+ },
80
+ ],
81
+ });
82
+
83
+ router.push(`/${SLUG}`);
84
+ await router.isReady();
85
+
86
+ const headerViewModel = mapToPageHeaderViewModel(
87
+ new PageHeaderMobile({ headerButton: options.headerButton }),
88
+ new PageStyles(),
89
+ [],
90
+ );
91
+
92
+ const context = createContext();
93
+
94
+ if (options.isPreview) {
95
+ context.isPreview = true;
96
+ }
97
+
98
+ return mount(NavigationSideMenu, {
99
+ propsData: {
100
+ context,
101
+ isSideMenuOpen: true,
102
+ slug: SLUG,
103
+ styles: headerViewModel.navigation,
104
+ headerButtonViewModel: headerViewModel.headerButtonViewModel,
105
+ },
106
+ global: { plugins: [router] },
107
+ });
108
+ };
109
+
110
+ describe('NavigationSideMenu - header call-to-action button (BP-5852)', () => {
111
+ beforeEach(() => {
112
+ setActivePinia(createPinia());
113
+ });
114
+
115
+ test('renders the CTA in the mobile side menu', async () => {
116
+ const wrapper = await mountSideMenu({
117
+ headerButton: aHeaderButton(),
118
+ });
119
+
120
+ expect(
121
+ wrapper.find('[data-component="navigation-side-menu-cta"]').exists(),
122
+ ).toBe(true);
123
+ expect(wrapper.get(CTA).text()).toContain('Book now');
124
+ });
125
+
126
+ test('renders no CTA when the site has no header button', async () => {
127
+ const wrapper = await mountSideMenu({});
128
+
129
+ expect(wrapper.find(CTA).exists()).toBe(false);
130
+ expect(
131
+ wrapper.find('[data-component="navigation-side-menu-cta"]').exists(),
132
+ ).toBe(false);
133
+ });
134
+
135
+ test('with a CTA the panel hands scrolling to the nav list and pins the button last', async () => {
136
+ const wrapper = await mountSideMenu({
137
+ headerButton: aHeaderButton(),
138
+ });
139
+
140
+ const sidebar = wrapper.get('.c-sidebar');
141
+
142
+ expect(sidebar.classes()).toContain('c-sidebar--has-cta');
143
+ expect(sidebar.classes()).not.toContain('tw-overflow-auto');
144
+ expect(sidebar.element.lastElementChild).toBe(
145
+ wrapper.get('.c-sidebar-cta').element,
146
+ );
147
+ });
148
+
149
+ test('without a CTA the panel keeps its original scrolling and no layout class', async () => {
150
+ const wrapper = await mountSideMenu({});
151
+
152
+ const sidebar = wrapper.get('.c-sidebar');
153
+
154
+ expect(sidebar.classes()).toContain('tw-overflow-auto');
155
+ expect(sidebar.classes()).not.toContain('c-sidebar--has-cta');
156
+ });
157
+
158
+ test('closes the side menu when the CTA is clicked', async () => {
159
+ const wrapper = await mountSideMenu({
160
+ headerButton: aHeaderButton(),
161
+ });
162
+
163
+ await wrapper.get(`${CTA} a`).trigger('click');
164
+
165
+ expect(wrapper.emitted('close-side-menu')).toHaveLength(1);
166
+ });
167
+
168
+ test('a preview CTA click is inert: no navigation, side menu stays open', async () => {
169
+ const wrapper = await mountSideMenu({
170
+ headerButton: aHeaderButton(),
171
+ isPreview: true,
172
+ });
173
+
174
+ const event = new MouseEvent('click', { bubbles: true, cancelable: true });
175
+
176
+ wrapper.get(`${CTA} a`).element.dispatchEvent(event);
177
+
178
+ expect(event.defaultPrevented).toBe(true);
179
+ expect(wrapper.emitted('close-side-menu')).toBeUndefined();
180
+ });
181
+ });
@@ -2,6 +2,7 @@ import type { PropType } from 'vue';
2
2
  import { defineComponent } from 'vue';
3
3
  import type {
4
4
  BrandPageViewModel,
5
+ ButtonViewModel,
5
6
  ContentViewModel,
6
7
  PageViewModel,
7
8
  } from '@brand-page/view-models';
@@ -14,6 +15,7 @@ import {
14
15
  DEFAULT_FONT_FAMILY_KEY,
15
16
  FALLBACK_FONT_FAMILIES,
16
17
  FONT_FAMILY_CONFIGS,
18
+ headerRowRendersCta,
17
19
  } from '../../constants';
18
20
  import {
19
21
  getSlugFromContext,
@@ -23,7 +25,10 @@ import {
23
25
  PAGE_SCOPE_ID_ATTRIBUTE,
24
26
  resolveColorReference,
25
27
  } from '@brand-page/utils';
26
- import { findNavItemByRelativePath } from '@brand-page/utils/route.util';
28
+ import {
29
+ findNavItemByRelativePath,
30
+ hasVisibleNavigation,
31
+ } from '@brand-page/utils/route.util';
27
32
  import type { ComponentPublicInstance } from '@vue/runtime-core';
28
33
  import { BRAND_PAGE_TYPES } from '@brand-page/models';
29
34
  import { hasProfileContent as checkHasProfileContent } from '@brand-page/utils/vcard.util';
@@ -370,17 +375,22 @@ export default defineComponent({
370
375
  );
371
376
  },
372
377
 
373
- shouldShowNavigation(): boolean {
374
- // Show navigation if more than 1 non-hidden page exists, or if there's 1 page with non-hidden subpages
375
- const visibleNavItems = this.context.nav.navItems.filter(
376
- (item) => !item.isHidden,
377
- );
378
+ // The button shows in the header row OR in the side menu, never both. This gives it to the side menu when the header doesn't show it; undefined = no button in the menu.
379
+ sideMenuHeaderButtonViewModel(): ButtonViewModel | undefined {
380
+ const header = this.viewModel.headerViewModelMobile;
378
381
 
379
- return (
380
- visibleNavItems.length > 1 ||
381
- (visibleNavItems.length === 1 &&
382
- visibleNavItems[0].nestedNavItems.some((item) => !item.isHidden))
383
- );
382
+ if (
383
+ !header.headerButtonViewModel ||
384
+ headerRowRendersCta(header.displayStyle, this.shouldShowNavigation)
385
+ ) {
386
+ return undefined;
387
+ }
388
+
389
+ return header.headerButtonViewModel;
390
+ },
391
+
392
+ shouldShowNavigation(): boolean {
393
+ return hasVisibleNavigation(this.context.nav.navItems);
384
394
  },
385
395
 
386
396
  showNavigationAfterLogoMobile(): boolean {
@@ -296,6 +296,7 @@
296
296
  :page-navigation="viewModel.nav"
297
297
  @close-side-menu="closeSideMenu"
298
298
  />
299
+ <!-- No header CTA: the desktop hamburger header row renders it. -->
299
300
  </template>
300
301
  <template #mobile>
301
302
  <NavigationSideMenu
@@ -304,6 +305,7 @@
304
305
  :slug="slug"
305
306
  :styles="viewModel.headerViewModelMobile.navigation"
306
307
  :page-navigation="viewModel.nav"
308
+ :header-button-view-model="sideMenuHeaderButtonViewModel"
307
309
  @close-side-menu="closeSideMenu"
308
310
  />
309
311
  </template>
@@ -43,6 +43,7 @@ const Template: StoryFn = (args) => ({
43
43
  nav-id="nav-1"
44
44
  :show-nav="args.showNav"
45
45
  :display-style="args.displayStyle"
46
+ :has-cta="args.hasCta"
46
47
  logo-width="100px"
47
48
  >
48
49
  <template v-slot:logo>
@@ -51,6 +52,9 @@ const Template: StoryFn = (args) => ({
51
52
  <template v-slot:nav>
52
53
  <div style="border: 1px solid red; display: inline-block;">Nav | Nav | Nav | Nav | Nav | Nav | Nav | Nav</div>
53
54
  </template>
55
+ <template v-slot:cta>
56
+ <div style="border: 1px solid green; display: inline-block; padding: 8px 16px;">Contact Us</div>
57
+ </template>
54
58
  </HeaderLayout>
55
59
  </div>
56
60
  `,
@@ -60,3 +64,9 @@ export const Sample = Template.bind({});
60
64
  Sample.args = {
61
65
  displayStyle: HEADER_DISPLAY.DESKTOP_TAB_5,
62
66
  };
67
+
68
+ export const WithHeaderButton = Template.bind({});
69
+ WithHeaderButton.args = {
70
+ displayStyle: HEADER_DISPLAY.DESKTOP_TAB_1,
71
+ hasCta: true,
72
+ };