@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
@@ -713,6 +713,17 @@ export class NavModel {
713
713
  }
714
714
  }
715
715
 
716
+ export interface HeaderButton {
717
+ isVisible: boolean;
718
+ text: string;
719
+ buttonLinkType: ButtonLinkType;
720
+ url?: string;
721
+ buttonPageId?: string;
722
+ buttonTextStyle?: TextStyleDeclaration;
723
+ fillColor?: string;
724
+ borderColor?: string;
725
+ }
726
+
716
727
  type AbstractPageHeaderStylesType = Partial<AbstractPageHeaderStyles>;
717
728
 
718
729
  export abstract class AbstractPageHeaderStyles {
@@ -742,6 +753,9 @@ export abstract class AbstractPageHeaderStyles {
742
753
  style: BrandPageCssStyle;
743
754
  viewport: ViewportsType;
744
755
 
756
+ /** Missing = no button. `declare` keeps it off saved data so old migrations still see the same shape. */
757
+ declare headerButton?: HeaderButton;
758
+
745
759
  protected constructor(options: AbstractPageHeaderStylesType = {}) {
746
760
  if (typeof options.viewport === 'undefined') {
747
761
  throw new Error('"viewport" is required');
@@ -779,6 +793,11 @@ export abstract class AbstractPageHeaderStyles {
779
793
  ...(options.backgroundStyle || {}),
780
794
  backgroundOpacity: options.backgroundStyle?.backgroundOpacity ?? 100,
781
795
  };
796
+
797
+ // Only set when given — a missing button must stay missing for old migrations.
798
+ if (options.headerButton) {
799
+ this.headerButton = options.headerButton;
800
+ }
782
801
  }
783
802
  }
784
803
 
@@ -17,3 +17,18 @@ export const getSlugFromContext = (context: BrandPageContext) => {
17
17
  */
18
18
  export const isPreviewMode = (context: BrandPageContext): boolean =>
19
19
  !!(context.isPreview || context.isLivePreview);
20
+
21
+ // Blocks a click in the maker preview (no link-follow, no bubbling); true when it fired.
22
+ export const guardPreviewClick = (
23
+ event: MouseEvent,
24
+ context: BrandPageContext,
25
+ ): boolean => {
26
+ if (!context.isPreview) {
27
+ return false;
28
+ }
29
+
30
+ event.preventDefault();
31
+ event.stopPropagation();
32
+
33
+ return true;
34
+ };
@@ -208,3 +208,16 @@ export const findNavItemByPageSlug = (
208
208
 
209
209
  return undefined;
210
210
  };
211
+
212
+ // Nav shows when more than one page is visible, or one page has visible subpages.
213
+ // The header row and the side menu both key the button placement off this — one
214
+ // source so the two can never disagree.
215
+ export const hasVisibleNavigation = (navItems: NavItemViewModel[]): boolean => {
216
+ const visibleNavItems = navItems.filter((item) => !item.isHidden);
217
+
218
+ return (
219
+ visibleNavItems.length > 1 ||
220
+ (visibleNavItems.length === 1 &&
221
+ visibleNavItems[0].nestedNavItems.some((item) => !item.isHidden))
222
+ );
223
+ };
@@ -5,8 +5,14 @@ import {
5
5
  type MailingListContent,
6
6
  type ContactFormContent,
7
7
  type ColorItem,
8
+ type HeaderButton,
9
+ } from '../models';
10
+ import {
11
+ BRAND_PAGE_TYPES,
12
+ BUTTON_LINK_TYPE,
13
+ PageHeaderDesktop,
14
+ PageStyles,
8
15
  } from '../models';
9
- import { BRAND_PAGE_TYPES, PageHeaderDesktop, PageStyles } from '../models';
10
16
  import {
11
17
  mapToBannerSectionViewModel,
12
18
  mapToContactFormViewModel,
@@ -1114,3 +1120,147 @@ describe('container-aware text colour defaults (view-model layer)', () => {
1114
1120
  expect(vm.nameStyle?.color).toBe('#00ff00');
1115
1121
  });
1116
1122
  });
1123
+
1124
+ describe('mapToPageHeaderViewModel header button', () => {
1125
+ const headerButton: HeaderButton = {
1126
+ isVisible: true,
1127
+ text: 'Contact Us',
1128
+ buttonLinkType: BUTTON_LINK_TYPE.url,
1129
+ url: 'https://example.com',
1130
+ buttonTextStyle: { textStyleType: 'buttons' },
1131
+ fillColor: '#123456',
1132
+ borderColor: '#654321',
1133
+ };
1134
+
1135
+ describe('headerButtonViewModel', () => {
1136
+ const mapButton = (overrides: Partial<HeaderButton> = {}) =>
1137
+ mapToPageHeaderViewModel(
1138
+ new PageHeaderDesktop({
1139
+ headerButton: { ...headerButton, ...overrides },
1140
+ }),
1141
+ new PageStyles(),
1142
+ [],
1143
+ ).headerButtonViewModel;
1144
+
1145
+ test('is absent when the header has no button at all', () => {
1146
+ const vm = mapToPageHeaderViewModel(
1147
+ new PageHeaderDesktop(),
1148
+ new PageStyles(),
1149
+ [],
1150
+ );
1151
+
1152
+ expect(vm.headerButtonViewModel).toBeUndefined();
1153
+ });
1154
+
1155
+ test('is absent when the button is configured but hidden', () => {
1156
+ expect(mapButton({ isVisible: false })).toBeUndefined();
1157
+ });
1158
+
1159
+ test('carries the authored label', () => {
1160
+ expect(mapButton({ text: 'Book now' })?.title).toBe('Book now');
1161
+ });
1162
+
1163
+ // Wrapped lines justify; CSS text-align-last keeps the only/last line centered.
1164
+ test('the label text-align is justify', () => {
1165
+ expect(mapButton({})?.titleStyle.textAlign).toBe('justify');
1166
+ });
1167
+
1168
+ // No label means no button — the maker seeds "Contact Us", so this is a deliberate clear.
1169
+ test.each([[''], [' ']])(
1170
+ 'a blank label hides the button (%j)',
1171
+ (text) => {
1172
+ expect(mapButton({ text })).toBeUndefined();
1173
+ },
1174
+ );
1175
+
1176
+ // Button.vue reads the link kind off `role` for non-section content, so the
1177
+ // authored link type has to land on the role/clickActionField pair.
1178
+ test.each([
1179
+ [BUTTON_LINK_TYPE.url, 'general-link', 'url'],
1180
+ [BUTTON_LINK_TYPE.page, 'general-link', 'page'],
1181
+ [BUTTON_LINK_TYPE.mailTo, 'email', 'url'],
1182
+ [BUTTON_LINK_TYPE.tel, 'phone', 'url'],
1183
+ ])(
1184
+ 'maps the %s link type to role %s / clickActionField %s',
1185
+ (buttonLinkType, role, clickActionField) => {
1186
+ const vm = mapButton({ buttonLinkType });
1187
+
1188
+ expect(vm?.role).toBe(role);
1189
+ expect(vm?.clickActionField).toBe(clickActionField);
1190
+ },
1191
+ );
1192
+
1193
+ test('routes a page link through the picked page id', () => {
1194
+ const vm = mapButton({
1195
+ buttonLinkType: BUTTON_LINK_TYPE.page,
1196
+ buttonPageId: 'page-2',
1197
+ url: undefined,
1198
+ });
1199
+
1200
+ expect(vm?.page).toBe('page-2');
1201
+ expect(vm?.pageSlug).toBe('page-2');
1202
+ });
1203
+
1204
+ test('overrides the theme fill and border with the authored colours', () => {
1205
+ const vm = mapButton({ fillColor: '#123456', borderColor: '#654321' });
1206
+
1207
+ expect(vm?.styles.style.backgroundColor).toBe('#123456');
1208
+ expect(vm?.styles.style.borderColor).toBe('#654321');
1209
+ });
1210
+
1211
+ test('keeps the site button style when no colours are authored', () => {
1212
+ const styles = new PageStyles();
1213
+ const vm = mapToPageHeaderViewModel(
1214
+ new PageHeaderDesktop({
1215
+ headerButton: {
1216
+ ...headerButton,
1217
+ fillColor: undefined,
1218
+ borderColor: undefined,
1219
+ },
1220
+ }),
1221
+ styles,
1222
+ [],
1223
+ ).headerButtonViewModel;
1224
+
1225
+ expect(vm?.styles.style.backgroundColor).toBe(
1226
+ styles.buttonStyles.style.backgroundColor,
1227
+ );
1228
+ expect(vm?.styles.selectedButtonStyleId).toBe(
1229
+ styles.buttonStyles.selectedButtonStyleId,
1230
+ );
1231
+ });
1232
+
1233
+ test('resolves the button text style against the site text styles', () => {
1234
+ const styles = new PageStyles();
1235
+
1236
+ styles.textStyles.textStyleOptions.buttons.style.fontFamily = 'Lato';
1237
+
1238
+ const vm = mapToPageHeaderViewModel(
1239
+ new PageHeaderDesktop({ headerButton }),
1240
+ styles,
1241
+ [],
1242
+ ).headerButtonViewModel;
1243
+
1244
+ expect(vm?.titleStyle?.fontFamily).toBe('Lato');
1245
+ });
1246
+
1247
+ // The maker writes buttonTextStyle only once the author touches the style field,
1248
+ // so an untouched button still has to inherit the site's button text style.
1249
+ test('defaults to the site button text style when none is authored', () => {
1250
+ const styles = new PageStyles();
1251
+
1252
+ styles.textStyles.textStyleOptions.buttons.style.fontFamily = 'Lato';
1253
+
1254
+ const vm = mapToPageHeaderViewModel(
1255
+ new PageHeaderDesktop({
1256
+ headerButton: { ...headerButton, buttonTextStyle: undefined },
1257
+ }),
1258
+ styles,
1259
+ [],
1260
+ ).headerButtonViewModel;
1261
+
1262
+ expect(vm?.titleStyle?.textStyleType).toBe('buttons');
1263
+ expect(vm?.titleStyle?.fontFamily).toBe('Lato');
1264
+ });
1265
+ });
1266
+ });
@@ -44,8 +44,11 @@ import type {
44
44
  NavRoute,
45
45
  NavOrder,
46
46
  ColorItem,
47
+ HeaderButton,
48
+ ButtonLinkType,
49
+ ContentType,
47
50
  } from '../models';
48
- import { ButtonStyles, VIEWPORTS } from '../models';
51
+ import { ButtonStyles, TEXT_STYLE_TYPES, VIEWPORTS } from '../models';
49
52
  import {
50
53
  NavigationMobile,
51
54
  NavigationDesktop,
@@ -1189,6 +1192,71 @@ export const mapToPageStyleViewModel = (
1189
1192
  };
1190
1193
  };
1191
1194
 
1195
+ // Maps each link kind to the role Button.vue already understands, so the button needs no new code.
1196
+ const HEADER_BUTTON_LINK_ROLES: Record<ButtonLinkType, ContentType> = {
1197
+ [BUTTON_LINK_TYPE.url]: 'general-link',
1198
+ [BUTTON_LINK_TYPE.page]: 'general-link',
1199
+ [BUTTON_LINK_TYPE.mailTo]: 'email',
1200
+ [BUTTON_LINK_TYPE.tel]: 'phone',
1201
+ };
1202
+
1203
+ const mapToHeaderButtonViewModel = (
1204
+ headerButton: HeaderButton | undefined,
1205
+ styles: PageStyles,
1206
+ ): ButtonViewModel | undefined => {
1207
+ // No label means no button — hidden in preview and on the published site.
1208
+ if (!headerButton?.isVisible || !headerButton.text?.trim()) {
1209
+ return undefined;
1210
+ }
1211
+
1212
+ const titleStyle: TextStyleDeclaration = headerButton.buttonTextStyle ?? {
1213
+ textStyleType: TEXT_STYLE_TYPES.buttons,
1214
+ };
1215
+
1216
+ const viewModel = mapToButtonViewModel(
1217
+ {
1218
+ id: 'page-header-button',
1219
+ // Old saved data may have no link kind — treat it as a url.
1220
+ type:
1221
+ HEADER_BUTTON_LINK_ROLES[headerButton.buttonLinkType] ??
1222
+ HEADER_BUTTON_LINK_ROLES[BUTTON_LINK_TYPE.url],
1223
+ iconUrl: '',
1224
+ modalImageUrl: '',
1225
+ colorReplacements: [],
1226
+ title: headerButton.text.trim(),
1227
+ titleStyle,
1228
+ url: headerButton.url ?? '',
1229
+ page: headerButton.buttonPageId ?? '',
1230
+ clickActionField:
1231
+ headerButton.buttonLinkType === BUTTON_LINK_TYPE.page ? 'page' : 'url',
1232
+ },
1233
+ styles,
1234
+ { titleStyle },
1235
+ );
1236
+
1237
+ return {
1238
+ ...viewModel,
1239
+ // A wrapped label justifies; the last (or only) line stays centered like a short label.
1240
+ titleStyle: {
1241
+ ...viewModel.titleStyle,
1242
+ textAlign: 'justify',
1243
+ textAlignLast: 'center',
1244
+ },
1245
+ styles: {
1246
+ ...viewModel.styles,
1247
+ style: {
1248
+ ...viewModel.styles.style,
1249
+ ...(headerButton.fillColor
1250
+ ? { backgroundColor: headerButton.fillColor }
1251
+ : {}),
1252
+ ...(headerButton.borderColor
1253
+ ? { borderColor: headerButton.borderColor }
1254
+ : {}),
1255
+ },
1256
+ },
1257
+ };
1258
+ };
1259
+
1192
1260
  export const mapToPageHeaderViewModel = (
1193
1261
  pageHeader: PageHeaderDesktop | PageHeaderMobile,
1194
1262
  pageStyles: PageStyles,
@@ -1283,6 +1351,10 @@ export const mapToPageHeaderViewModel = (
1283
1351
  }),
1284
1352
  style: pageHeader.style,
1285
1353
  backgroundStyle: getHeaderBackgroundStyleWithOpacity(pageHeader),
1354
+ headerButtonViewModel: mapToHeaderButtonViewModel(
1355
+ pageHeader.headerButton,
1356
+ pageStyles,
1357
+ ),
1286
1358
  };
1287
1359
  };
1288
1360
 
@@ -259,11 +259,14 @@ export interface PageHeaderViewModel extends Omit<
259
259
  | 'logoTemplateImage'
260
260
  | 'logoImageHeight'
261
261
  | 'logoImageWidth'
262
+ | 'headerButton'
262
263
  | 'backgroundStyle'
263
264
  > {
264
265
  logo: LogoViewModel;
265
266
  pages: PageViewModel[];
266
267
  backgroundStyle: BrandPageCssStyle;
268
+ /** Absent unless the header call-to-action button is configured AND visible. */
269
+ headerButtonViewModel?: ButtonViewModel;
267
270
  }
268
271
 
269
272
  export interface PageFooterViewModel extends Omit<
@@ -38,14 +38,15 @@ export const textValidator: ContentValidator<BannerSectionContent> = (
38
38
  };
39
39
  };
40
40
 
41
- export const urlValidator: ContentValidator<
42
- BannerSectionContent | BookingSectionContent
43
- > = (section): ValidateResult => {
41
+ const VALID: ValidateResult = { message: '', isValid: true };
42
+
43
+ // The link rules alone — the header button uses these directly (its link is always required).
44
+ export const buttonLinkValidator = (
45
+ section: Pick<BannerSectionContent, 'buttonLinkType' | 'url'>,
46
+ ): ValidateResult => {
44
47
  if (
45
48
  section.buttonLinkType === BUTTON_LINK_TYPE.url &&
46
- (!isURL(section.url) || containsAtSymbol(section.url)) &&
47
- section.buttonText !== false &&
48
- section.buttonText?.length > 0
49
+ (!isURL(section.url) || containsAtSymbol(section.url))
49
50
  ) {
50
51
  return {
51
52
  message: MISSING_BUTTON_URL_MESSAGE(),
@@ -56,9 +57,7 @@ export const urlValidator: ContentValidator<
56
57
  if (
57
58
  section.buttonLinkType === BUTTON_LINK_TYPE.mailTo &&
58
59
  typeof section.url === 'string' &&
59
- !isEmail(section.url) &&
60
- section.buttonText !== false &&
61
- section.buttonText?.length > 0
60
+ !isEmail(section.url)
62
61
  ) {
63
62
  return {
64
63
  message: MISSING_BUTTON_EMAIL_MESSAGE(),
@@ -66,24 +65,29 @@ export const urlValidator: ContentValidator<
66
65
  };
67
66
  }
68
67
 
69
- if (
70
- section.buttonLinkType === BUTTON_LINK_TYPE.tel &&
71
- !isTel(section.url) &&
72
- section.buttonText !== false &&
73
- section.buttonText?.length > 0
74
- ) {
68
+ if (section.buttonLinkType === BUTTON_LINK_TYPE.tel && !isTel(section.url)) {
75
69
  return {
76
70
  message: MISSING_BUTTON_TEL_MESSAGE(),
77
71
  isValid: false,
78
72
  };
79
73
  }
80
74
 
81
- return {
82
- message: '',
83
- isValid: true,
84
- };
75
+ return VALID;
85
76
  };
86
77
 
78
+ type ButtonLinkFields = Pick<
79
+ BannerSectionContent,
80
+ 'buttonLinkType' | 'url' | 'buttonText'
81
+ >;
82
+
83
+ // The banner's rule: its button text is optional, and only a button with text needs a link.
84
+ export const urlValidator: ContentValidator<ButtonLinkFields> = (
85
+ section,
86
+ ): ValidateResult =>
87
+ section.buttonText !== false && section.buttonText?.length > 0
88
+ ? buttonLinkValidator(section)
89
+ : VALID;
90
+
87
91
  export const pageValidator: ContentValidator<
88
92
  BannerSectionContent | BookingSectionContent
89
93
  > = (section): ValidateResult => {
@@ -1867,7 +1867,12 @@
1867
1867
  "iconStyle": "Symbol-Stil",
1868
1868
  "logoSize": "Logogröße",
1869
1869
  "socialLinks": "Social-Media-Links",
1870
- "copyright": "Copyright"
1870
+ "copyright": "Copyright",
1871
+ "buttonLabel": "Etikett",
1872
+ "buttonLabelPlaceholder": "Kontakt",
1873
+ "buttonLink": "Link",
1874
+ "buttonFillColor": "Füllfarbe",
1875
+ "buttonBorderColor": "Randfarbe"
1871
1876
  },
1872
1877
  "publishBrandPageModalWithState": {
1873
1878
  "domainSearchButtonLabel": "Suche",
@@ -1867,7 +1867,12 @@
1867
1867
  "iconStyle": "Estilo de icono",
1868
1868
  "logoSize": "Tamaño del logo",
1869
1869
  "socialLinks": "Enlaces a redes sociales",
1870
- "copyright": "Derechos de autor"
1870
+ "copyright": "Derechos de autor",
1871
+ "buttonLabel": "Etiqueta",
1872
+ "buttonLabelPlaceholder": "Contacto",
1873
+ "buttonLink": "Enlace",
1874
+ "buttonFillColor": "Color de relleno",
1875
+ "buttonBorderColor": "Color del borde"
1871
1876
  },
1872
1877
  "publishBrandPageModalWithState": {
1873
1878
  "domainSearchButtonLabel": "Buscar",
@@ -1867,7 +1867,12 @@
1867
1867
  "iconStyle": "Style d'icône",
1868
1868
  "logoSize": "Taille du logo",
1869
1869
  "socialLinks": "Liens vers les réseaux sociaux",
1870
- "copyright": "Droits d’auteur"
1870
+ "copyright": "Droits d’auteur",
1871
+ "buttonLabel": "Étiquette",
1872
+ "buttonLabelPlaceholder": "Nous joindre",
1873
+ "buttonLink": "Lien",
1874
+ "buttonFillColor": "Couleur de remplissage",
1875
+ "buttonBorderColor": "Couleur de bordure"
1871
1876
  },
1872
1877
  "publishBrandPageModalWithState": {
1873
1878
  "domainSearchButtonLabel": "Rechercher",
@@ -1867,7 +1867,12 @@
1867
1867
  "iconStyle": "Style d'icône",
1868
1868
  "logoSize": "Taille du logo",
1869
1869
  "socialLinks": "Liens vers les réseaux sociaux",
1870
- "copyright": "Droits d’auteur"
1870
+ "copyright": "Droits d’auteur",
1871
+ "buttonLabel": "Étiquette",
1872
+ "buttonLabelPlaceholder": "Contactez-nous",
1873
+ "buttonLink": "Lien",
1874
+ "buttonFillColor": "Couleur de remplissage",
1875
+ "buttonBorderColor": "Couleur de bordure"
1871
1876
  },
1872
1877
  "publishBrandPageModalWithState": {
1873
1878
  "domainSearchButtonLabel": "Rechercher",
@@ -1867,7 +1867,12 @@
1867
1867
  "iconStyle": "Icon style",
1868
1868
  "logoSize": "Logo Size",
1869
1869
  "socialLinks": "Social Links",
1870
- "copyright": "Copyright"
1870
+ "copyright": "Copyright",
1871
+ "buttonLabel": "Label",
1872
+ "buttonLabelPlaceholder": "Contact Us",
1873
+ "buttonLink": "Link",
1874
+ "buttonFillColor": "Fill color",
1875
+ "buttonBorderColor": "Border color"
1871
1876
  },
1872
1877
  "publishBrandPageModalWithState": {
1873
1878
  "domainSearchButtonLabel": "Search",
@@ -1867,7 +1867,12 @@
1867
1867
  "iconStyle": "Estilo de ícone",
1868
1868
  "logoSize": "Tamanho do logotipo",
1869
1869
  "socialLinks": "Links de redes sociais",
1870
- "copyright": "Direitos autorais"
1870
+ "copyright": "Direitos autorais",
1871
+ "buttonLabel": "Rótulo",
1872
+ "buttonLabelPlaceholder": "Entre em contato",
1873
+ "buttonLink": "Link",
1874
+ "buttonFillColor": "Cor de preenchimento",
1875
+ "buttonBorderColor": "Cor da borda"
1871
1876
  },
1872
1877
  "publishBrandPageModalWithState": {
1873
1878
  "domainSearchButtonLabel": "Pesquisar",
@@ -1867,7 +1867,12 @@
1867
1867
  "iconStyle": "Estilo de Ícone",
1868
1868
  "logoSize": "Tamanho do logótipo",
1869
1869
  "socialLinks": "Links de redes sociais",
1870
- "copyright": "Direitos de autor"
1870
+ "copyright": "Direitos de autor",
1871
+ "buttonLabel": "Etiqueta",
1872
+ "buttonLabelPlaceholder": "Contacte-nos",
1873
+ "buttonLink": "Link",
1874
+ "buttonFillColor": "Cor de preenchimento",
1875
+ "buttonBorderColor": "Cor da borda"
1871
1876
  },
1872
1877
  "publishBrandPageModalWithState": {
1873
1878
  "domainSearchButtonLabel": "Pesquisar",
@@ -1,5 +1,20 @@
1
1
  import { defineComponent } from 'vue';
2
2
  import GenericTextInputMixin from '../GenericTextInput.mixin';
3
+
3
4
  export default defineComponent({
4
5
  mixins: [GenericTextInputMixin],
6
+ props: {
7
+ // Opt-in: clearing the field shows its error at once, not only after blur.
8
+ showErrorWhenEmpty: {
9
+ type: Boolean,
10
+ default: false,
11
+ },
12
+ },
13
+ watch: {
14
+ value(value: string | false | undefined): void {
15
+ if (this.showErrorWhenEmpty && !(value || '').trim()) {
16
+ this.isDirty = false;
17
+ }
18
+ },
19
+ },
5
20
  });
@@ -11,6 +11,7 @@
11
11
  :icon="icon"
12
12
  :error="isErrorVisible"
13
13
  @focus="onFocusOverride"
14
+ @blur="onBlur"
14
15
  @input="onInputOverride"
15
16
  @keyup="onChangeOverride"
16
17
  />
@@ -81,6 +81,22 @@ export const usePageNavigationStore = defineStore('pageNavigation', {
81
81
  orders: this.orders,
82
82
  });
83
83
  },
84
+ // Mirrors the renderer's hasVisibleNavigation (route.util.ts) on the maker's nav state:
85
+ // the header shows a nav for >1 visible top-level page, or 1 with a visible subpage.
86
+ hasVisibleNavigation(): boolean {
87
+ const isVisible = (order: NavOrder): boolean => {
88
+ const route = this.routes.find((r) => r.id === order.routeId);
89
+
90
+ return route !== undefined && !route.isHidden;
91
+ };
92
+ const visibleOrders = this.orders.filter(isVisible);
93
+
94
+ return (
95
+ visibleOrders.length > 1 ||
96
+ (visibleOrders.length === 1 &&
97
+ visibleOrders[0].subRoutes.some(isVisible))
98
+ );
99
+ },
84
100
  },
85
101
  actions: {
86
102
  getPageSlugsForDepth(parentRouteId?: string): string[] {