@designcrowd/library.brand-page 5.18.1 → 5.18.2-ast-padding-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 (108) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DFpjIAUv.js → CreatedOnBrandCrowd-de-de-GFYl8DtL.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-BapIT2xO.js → CreatedOnBrandCrowd-en-us-D5CcXf2I.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DaSf3Dt6.js → CreatedOnBrandCrowd-es-es-Cr7J3ooe.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Cvgei7yn.js → CreatedOnBrandCrowd-fr-ca-BC1NFFIW.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Cvgei7yn.js → CreatedOnBrandCrowd-fr-fr-BC1NFFIW.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Duxd2NmR.js → CreatedOnBrandCrowd-pt-br-BdiCcpwi.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Duxd2NmR.js → CreatedOnBrandCrowd-pt-pt-BdiCcpwi.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-DXwNbruv.js → CreatedOnDcom-de-de-d4TmC_lo.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-CkNGwN-p.js → CreatedOnDcom-en-us-DlvLEF-R.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-Csyf5yhI.js → CreatedOnDcom-es-es-BulpVp3k.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-GOIUmIdy.js → CreatedOnDcom-fr-ca-M4xqunZe.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DF63cL2a.js → CreatedOnDcom-fr-fr-BFqUXgzb.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-Ct6FWj7D.js → CreatedOnDcom-pt-br-CUVcVkV2.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-D22yb8Ci.js → CreatedOnDcom-pt-pt-Dc4CZ4xW.js} +1 -1
  15. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.d.ts +4 -0
  16. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +8 -4
  17. package/lib/src/brand-page/components/image-gallery/ImageGallery.mixin.d.ts +4 -0
  18. package/lib/src/brand-page/components/image-gallery/ImageGallery.mixin.js +4 -0
  19. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.d.ts +4 -0
  20. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +4 -1
  21. package/lib/src/brand-page/components/page/Page.mixin.d.ts +1 -1
  22. package/lib/src/brand-page/components/page/PageComponent.mixin.d.ts +1 -1
  23. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.d.ts +4 -0
  24. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.js +4 -0
  25. package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.d.ts +4 -0
  26. package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.js +4 -1
  27. package/lib/src/brand-page/components/video/Video.mixin.d.ts +4 -0
  28. package/lib/src/brand-page/components/video/Video.mixin.js +4 -0
  29. package/lib/src/brand-page/constants/misc.constants.d.ts +17 -0
  30. package/lib/src/brand-page/constants/misc.constants.js +25 -0
  31. package/lib/src/brand-page/models/content.model.d.ts +2 -0
  32. package/lib/src/brand-page/utils/content-padding.util.d.ts +13 -0
  33. package/lib/src/brand-page/utils/content-padding.util.js +10 -0
  34. package/lib/src/brand-page/utils/index.d.ts +1 -0
  35. package/lib/src/brand-page/utils/index.js +1 -0
  36. package/lib/src/brand-page/utils/migration/migration-16.d.ts +2 -0
  37. package/lib/src/brand-page/utils/migration/migration-16.js +37 -0
  38. package/lib/src/brand-page/utils/migration.util.js +6 -0
  39. package/lib/src/brand-page-maker/components/admin-logo-settings/AdminLogoSettings.mixin.d.ts +2 -0
  40. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +12 -0
  41. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +15 -1
  42. package/lib/src/brand-page-maker/constants/config-settings-constant.js +34 -0
  43. package/lib/src/brand-page-maker/convertors/banner-align-display-style.convertor.js +56 -2
  44. package/lib/src/brand-page-maker/convertors/contact-layout-style.convertor.js +6 -1
  45. package/lib/src/brand-page-maker/models/content-settings.js +30 -2
  46. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +12 -0
  47. package/lib/src/{bundled-translations-CnareLWC.js → bundled-translations-DYd5oFZv.js} +1 -1
  48. package/lib/src/bundles/bundled-translations.json +2 -0
  49. package/lib/src/css/library.brand-page-brandCrowd.min.css +152 -140
  50. package/lib/src/css/library.brand-page-brandPage.min.css +153 -140
  51. package/lib/src/css/library.brand-page-designCom.min.css +152 -140
  52. package/lib/src/css/library.brand-page-preview.min.css +152 -140
  53. package/lib/src/index.mjs +1 -1
  54. package/lib/src/{lib-_iwoSD1Z.js → lib-CRihSxY0.js} +20195 -19572
  55. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +14 -0
  56. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +14 -0
  57. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +12 -0
  58. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +12 -0
  59. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +12 -0
  60. package/lib/src/shared/content-settings/components/slider-field/SliderField.mixin.js +9 -0
  61. package/lib/src/shared/content-settings/store/content-settings-store.d.ts +12 -0
  62. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +16 -0
  63. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +14 -0
  64. package/lib/src/utils/minify-brand-page-metadata.js +11 -1
  65. package/package.json +2 -2
  66. package/src/brand-page/components/banner-section/BannerSection.vue +360 -12
  67. package/src/brand-page/components/banner-section/BannerSection.vue.test.ts +201 -0
  68. package/src/brand-page/components/booking-section/BookingSection.vue +11 -3
  69. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +12 -6
  70. package/src/brand-page/components/contact-form/ContactForm.vue +115 -118
  71. package/src/brand-page/components/image-gallery/ImageGallery.mixin.ts +5 -0
  72. package/src/brand-page/components/image-gallery/ImageGallery.vue +1 -1
  73. package/src/brand-page/components/mailing-list/MailingList.mixin.ts +8 -1
  74. package/src/brand-page/components/mailing-list/MailingList.vue +1 -1
  75. package/src/brand-page/components/map/Map.vue +5 -1
  76. package/src/brand-page/components/payment/request-payment/RequestPayment.mixin.ts +5 -0
  77. package/src/brand-page/components/payment/request-payment/RequestPayment.vue +1 -0
  78. package/src/brand-page/components/payment/support-me/SupportMe.mixin.ts +8 -1
  79. package/src/brand-page/components/payment/support-me/SupportMe.vue +1 -0
  80. package/src/brand-page/components/placeholder/Placeholder.vue +39 -100
  81. package/src/brand-page/components/social-links/SocialLinks.vue +7 -3
  82. package/src/brand-page/components/text/Text.vue +12 -5
  83. package/src/brand-page/components/text-media-section/TextMediaSection.vue +11 -3
  84. package/src/brand-page/components/video/Video.mixin.ts +5 -0
  85. package/src/brand-page/components/video/Video.vue +1 -1
  86. package/src/brand-page/constants/misc.constants.ts +26 -0
  87. package/src/brand-page/models/content.model.ts +2 -0
  88. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +7 -3
  89. package/src/brand-page/utils/content-padding.util.test.ts +29 -0
  90. package/src/brand-page/utils/content-padding.util.ts +13 -0
  91. package/src/brand-page/utils/index.ts +1 -0
  92. package/src/brand-page/utils/migration/migration-16.ts +51 -0
  93. package/src/brand-page/utils/migration.util.test.ts +64 -0
  94. package/src/brand-page/utils/migration.util.ts +7 -0
  95. package/src/brand-page-maker/constants/config-settings-constant.ts +40 -0
  96. package/src/brand-page-maker/convertors/banner-align-display-style.convertor.ts +61 -1
  97. package/src/brand-page-maker/convertors/contact-layout-style.convertor.test.ts +50 -0
  98. package/src/brand-page-maker/convertors/contact-layout-style.convertor.ts +8 -0
  99. package/src/brand-page-maker/models/content-settings.ts +32 -2
  100. package/src/bundles/bundled-translations.json +2 -0
  101. package/src/shared/content-settings/components/content-settings/i18n/content-settings.json +2 -0
  102. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.vue +1 -1
  103. package/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.vue +2 -2
  104. package/src/shared/content-settings/components/slider-field/SliderField.mixin.ts +9 -0
  105. package/src/utils/minify-brand-page-metadata.ts +11 -1
  106. package/lib/src/brand-page/components/banner-section/BannerSection.mixin.d.ts +0 -103
  107. package/lib/src/brand-page/components/banner-section/BannerSection.mixin.js +0 -178
  108. package/src/brand-page/components/banner-section/BannerSection.mixin.ts +0 -213
@@ -6,7 +6,8 @@
6
6
  'c_banner--with-image-overlay tw-flex tw-flex-col tw-justify-center':
7
7
  viewModel.isBigImageType,
8
8
  'c_banner--with-image-overlay tw-flex tw-flex-col': isDividedContent,
9
- 'tw-px-6 tw-py-8': !isDividedContent,
9
+ 'tw-px-6': !isDividedContent,
10
+ 'tw-py-8': !isDividedContent && !isStackedFullWidth,
10
11
  }"
11
12
  :style="viewModel.backgroundStyle"
12
13
  style="container-type: inline-size"
@@ -44,7 +45,85 @@
44
45
  "
45
46
  />
46
47
  <div
47
- v-if="!isDividedContent"
48
+ v-if="isStackedImageLayout"
49
+ class="tw-flex tw-flex-col tw-gap-5 tw-items-center tw-w-full tw-h-full c_banner-container"
50
+ >
51
+ <div
52
+ v-if="stackedImagePosition !== 'top'"
53
+ class="tw-flex tw-flex-col tw-gap-5 tw-items-center tw-w-full !tw-text-center"
54
+ >
55
+ <TextWrapper
56
+ v-if="viewModel.title"
57
+ :text-style="viewModel.titleStyle"
58
+ :text="viewModel.title"
59
+ :slug="context.brandPageSlug"
60
+ element="h1"
61
+ />
62
+ <template v-if="stackedImagePosition === 'bottom'">
63
+ <TextWrapper
64
+ v-if="hasDescriptionText"
65
+ :text-style="viewModel.descriptionStyle"
66
+ :text="renderText"
67
+ :slug="context.brandPageSlug"
68
+ element="div"
69
+ />
70
+ <div class="tw-w-full tw-flex" :class="displayButtonClass">
71
+ <Button
72
+ v-if="shouldShowButton"
73
+ :view-model="buttonViewModel"
74
+ :context="context"
75
+ :content-index="contentIndex"
76
+ class="tw-w-fit"
77
+ />
78
+ </div>
79
+ </template>
80
+ </div>
81
+
82
+ <div :class="stackedImageWrapperClass">
83
+ <Media
84
+ :image-url="viewModel.imageUrl"
85
+ :context="context"
86
+ :color-replacements="viewModel.colorReplacements"
87
+ :media-type="viewModel.mediaType"
88
+ :size="viewModel.size"
89
+ :role="viewModel.role"
90
+ img-classes="tw-w-full tw-aspect-[3/1] tw-object-cover tw-block"
91
+ :content-index="contentIndex"
92
+ :media="viewModel.mediaViewModel"
93
+ />
94
+ </div>
95
+
96
+ <div
97
+ v-if="stackedImagePosition !== 'bottom'"
98
+ class="tw-flex tw-flex-col tw-gap-5 tw-items-center tw-w-full !tw-text-center"
99
+ >
100
+ <TextWrapper
101
+ v-if="stackedImagePosition === 'top' && viewModel.title"
102
+ :text-style="viewModel.titleStyle"
103
+ :text="viewModel.title"
104
+ :slug="context.brandPageSlug"
105
+ element="h1"
106
+ />
107
+ <TextWrapper
108
+ v-if="hasDescriptionText"
109
+ :text-style="viewModel.descriptionStyle"
110
+ :text="renderText"
111
+ :slug="context.brandPageSlug"
112
+ element="div"
113
+ />
114
+ <div class="tw-w-full tw-flex" :class="displayButtonClass">
115
+ <Button
116
+ v-if="shouldShowButton"
117
+ :view-model="buttonViewModel"
118
+ :context="context"
119
+ :content-index="contentIndex"
120
+ class="tw-w-fit"
121
+ />
122
+ </div>
123
+ </div>
124
+ </div>
125
+ <div
126
+ v-else-if="!isDividedContent"
48
127
  :class="{
49
128
  'c_banner-container--left-aligned-stacking':
50
129
  viewModel.displayStyle === 'left-aligned',
@@ -101,7 +180,7 @@
101
180
  <div class="tw-w-full tw-flex" :class="displayButtonClass">
102
181
  <Button
103
182
  v-if="shouldShowButton"
104
- :view-model="getbuttonViewModel"
183
+ :view-model="buttonViewModel"
105
184
  :context="context"
106
185
  :content-index="contentIndex"
107
186
  :class="{
@@ -112,7 +191,7 @@
112
191
  </div>
113
192
  </div>
114
193
  </div>
115
- <div v-if="isDividedContent">
194
+ <div v-else-if="isDividedContent">
116
195
  <div
117
196
  v-if="!shouldStackOnTop"
118
197
  class="tw-flex tw-h-full tw-relative c_banner--divided-container"
@@ -142,11 +221,10 @@
142
221
  <div class="tw-w-full tw-flex" :class="displayButtonClass">
143
222
  <Button
144
223
  v-if="shouldShowButton"
145
- :view-model="getbuttonViewModel"
224
+ :view-model="buttonViewModel"
146
225
  :context="context"
147
226
  :content-index="contentIndex"
148
227
  :class="{
149
- 'tw-w-fit': !isHorizontalAligned,
150
228
  'tw-w-fit': isDividedContent,
151
229
  'c_banner-button--stacking': isHorizontalAligned,
152
230
  }"
@@ -227,7 +305,7 @@
227
305
  <div class="tw-w-full tw-flex" :class="displayButtonClass">
228
306
  <Button
229
307
  v-if="shouldShowButton"
230
- :view-model="getbuttonViewModel"
308
+ :view-model="buttonViewModel"
231
309
  :context="context"
232
310
  :content-index="contentIndex"
233
311
  :class="{
@@ -242,16 +320,273 @@
242
320
  </div>
243
321
  </section>
244
322
  </template>
245
- <script>
246
- import BannerSectionMixin from './BannerSection.mixin.ts';
323
+ <script setup lang="ts">
324
+ import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
325
+ import type { PropType } from 'vue';
247
326
  import Button from '../button/Button.vue';
248
327
  import Media from '../media/Media.vue';
249
328
  import TextWrapper from '../text-wrapper/TextWrapper.vue';
329
+ import type {
330
+ BannerSectionViewModel,
331
+ ButtonViewModel,
332
+ } from '@brand-page/view-models';
333
+ import type { BrandPageContext } from '@brand-page/models';
334
+ import { hasValidButton } from '../../../utils/validator.util';
335
+ import { getRenderText, escapeContentText } from '@brand-page/utils';
336
+ import { hasRTEContent } from '@brand-page-maker/utils';
337
+ import {
338
+ ALIGN_CENTERED,
339
+ ALIGN_LEFT_ALIGNED,
340
+ ALIGN_RIGHT_ALIGNED,
341
+ BIG_IMAGE,
342
+ ALIGN_LEFT_BIG_IMAGE,
343
+ ALIGN_RIGHT_FULL_IMAGE,
344
+ ALIGN_LEFT_FULL_IMAGE,
345
+ ALIGN_CARD_CENTER_IMAGE,
346
+ ALIGN_CARD_LEFT_IMAGE,
347
+ ALIGN_TOP_CARD_IMAGE,
348
+ ALIGN_BOTTOM_CARD_IMAGE,
349
+ ALIGN_MIDDLE_CARD_IMAGE,
350
+ ALIGN_TOP_FULL_IMAGE,
351
+ ALIGN_BOTTOM_FULL_IMAGE,
352
+ ALIGN_MIDDLE_FULL_IMAGE,
353
+ } from '../../constants';
354
+
355
+ type BannerLayout = 'inline' | 'divided' | 'stacked';
356
+ type StackedImagePosition = 'top' | 'middle' | 'bottom';
357
+
358
+ interface BannerLayoutConfig {
359
+ layout: BannerLayout;
360
+ /** Alignment/width classes for the text column (inline and divided-mobile layouts). */
361
+ alignClass: string;
362
+ /** Renders the 1/3 side image and the container-query stacking classes. */
363
+ hasSideImage?: boolean;
364
+ /** Text content rendered inside a card (cardStyle background, padding, rounding). */
365
+ isCard?: boolean;
366
+ isCardAlignedOnLeft?: boolean;
367
+ stackedImagePosition?: StackedImagePosition;
368
+ /** Stacked image bleeds edge-to-edge instead of staying inside the max-width container. */
369
+ isStackedFullWidth?: boolean;
370
+ }
371
+
372
+ const DEFAULT_LAYOUT: BannerLayoutConfig = { layout: 'inline', alignClass: '' };
250
373
 
251
- export default {
252
- components: { Button, Media, TextWrapper },
253
- mixins: [BannerSectionMixin],
374
+ const BANNER_LAYOUTS: Record<string, BannerLayoutConfig> = {
375
+ [ALIGN_CENTERED]: {
376
+ layout: 'inline',
377
+ alignClass: 'tw-items-center tw-w-full',
378
+ },
379
+ [BIG_IMAGE]: {
380
+ layout: 'inline',
381
+ alignClass: 'tw-items-center tw-w-full',
382
+ },
383
+ [ALIGN_LEFT_ALIGNED]: {
384
+ layout: 'inline',
385
+ alignClass: 'tw-items-start tw-w-2/3',
386
+ hasSideImage: true,
387
+ },
388
+ [ALIGN_RIGHT_ALIGNED]: {
389
+ layout: 'inline',
390
+ alignClass: 'tw-items-end tw-w-2/3',
391
+ hasSideImage: true,
392
+ },
393
+ [ALIGN_LEFT_BIG_IMAGE]: {
394
+ layout: 'inline',
395
+ alignClass: 'tw-items-start tw-w-2/3',
396
+ },
397
+ [ALIGN_CARD_CENTER_IMAGE]: {
398
+ layout: 'inline',
399
+ alignClass: 'tw-items-center tw-w-full',
400
+ isCard: true,
401
+ },
402
+ [ALIGN_CARD_LEFT_IMAGE]: {
403
+ layout: 'inline',
404
+ alignClass: 'tw-items-end',
405
+ isCard: true,
406
+ isCardAlignedOnLeft: true,
407
+ },
408
+ [ALIGN_LEFT_FULL_IMAGE]: {
409
+ layout: 'divided',
410
+ alignClass: '',
411
+ hasSideImage: true,
412
+ },
413
+ [ALIGN_RIGHT_FULL_IMAGE]: {
414
+ layout: 'divided',
415
+ alignClass: '',
416
+ hasSideImage: true,
417
+ },
418
+ [ALIGN_TOP_CARD_IMAGE]: {
419
+ layout: 'stacked',
420
+ alignClass: '',
421
+ stackedImagePosition: 'top',
422
+ },
423
+ [ALIGN_BOTTOM_CARD_IMAGE]: {
424
+ layout: 'stacked',
425
+ alignClass: '',
426
+ stackedImagePosition: 'bottom',
427
+ },
428
+ [ALIGN_MIDDLE_CARD_IMAGE]: {
429
+ layout: 'stacked',
430
+ alignClass: '',
431
+ stackedImagePosition: 'middle',
432
+ },
433
+ [ALIGN_TOP_FULL_IMAGE]: {
434
+ layout: 'stacked',
435
+ alignClass: '',
436
+ stackedImagePosition: 'top',
437
+ isStackedFullWidth: true,
438
+ },
439
+ [ALIGN_BOTTOM_FULL_IMAGE]: {
440
+ layout: 'stacked',
441
+ alignClass: '',
442
+ stackedImagePosition: 'bottom',
443
+ isStackedFullWidth: true,
444
+ },
445
+ [ALIGN_MIDDLE_FULL_IMAGE]: {
446
+ layout: 'stacked',
447
+ alignClass: '',
448
+ stackedImagePosition: 'middle',
449
+ isStackedFullWidth: true,
450
+ },
254
451
  };
452
+
453
+ const props = defineProps({
454
+ viewModel: {
455
+ type: Object as PropType<BannerSectionViewModel & { pageSlug?: string }>,
456
+ required: true,
457
+ },
458
+ context: {
459
+ type: Object as PropType<BrandPageContext>,
460
+ required: true,
461
+ },
462
+ containerClientWidth: {
463
+ type: Number as PropType<number | undefined>,
464
+ required: false,
465
+ default: undefined,
466
+ },
467
+ containerClientHeight: {
468
+ type: Number as PropType<number | undefined>,
469
+ required: false,
470
+ default: undefined,
471
+ },
472
+ contentIndex: {
473
+ type: Number,
474
+ required: false,
475
+ default: undefined,
476
+ },
477
+ isPreview: {
478
+ type: Boolean,
479
+ required: false,
480
+ default: false,
481
+ },
482
+ isMobileDevice: {
483
+ type: Boolean,
484
+ required: false,
485
+ default: undefined,
486
+ },
487
+ });
488
+
489
+ const layout = computed<BannerLayoutConfig>(
490
+ () => BANNER_LAYOUTS[props.viewModel.displayStyle] ?? DEFAULT_LAYOUT,
491
+ );
492
+
493
+ const displayAlignClass = computed(() => layout.value.alignClass);
494
+ const isHorizontalAligned = computed(() => !!layout.value.hasSideImage);
495
+ const isDividedContent = computed(() => layout.value.layout === 'divided');
496
+ const isCardBannerContent = computed(() => !!layout.value.isCard);
497
+ const isCardAlignedOnLeft = computed(() => !!layout.value.isCardAlignedOnLeft);
498
+ const isStackedImageLayout = computed(() => layout.value.layout === 'stacked');
499
+ const stackedImagePosition = computed<StackedImagePosition>(
500
+ () => layout.value.stackedImagePosition ?? 'top',
501
+ );
502
+ const isStackedFullWidth = computed(() => !!layout.value.isStackedFullWidth);
503
+
504
+ const isRightImageAligned = computed(
505
+ () => props.viewModel.displayStyle === ALIGN_RIGHT_FULL_IMAGE,
506
+ );
507
+
508
+ const stackedImageWrapperClass = computed<Record<string, boolean>>(() => ({
509
+ 'tw-w-full': !isStackedFullWidth.value,
510
+ 'c_banner-stacked-image--full': isStackedFullWidth.value,
511
+ 'tw-rounded-md tw-overflow-hidden': !isStackedFullWidth.value,
512
+ }));
513
+
514
+ const displayButtonClass = computed(() => {
515
+ const { buttonTextStyle, displayStyle } = props.viewModel;
516
+
517
+ if (buttonTextStyle) {
518
+ switch (buttonTextStyle.textAlign) {
519
+ case 'left':
520
+ return 'tw-justify-start';
521
+ case 'center':
522
+ return 'tw-justify-center';
523
+ case 'right':
524
+ return 'tw-justify-end';
525
+ default:
526
+ return '';
527
+ }
528
+ }
529
+
530
+ return displayStyle === ALIGN_CARD_CENTER_IMAGE ||
531
+ displayStyle === ALIGN_CENTERED
532
+ ? 'tw-justify-center'
533
+ : 'tw-justify-start';
534
+ });
535
+
536
+ const buttonViewModel = computed<ButtonViewModel>(() => {
537
+ const { buttonViewModel: model, buttonLinkType, pageSlug } = props.viewModel;
538
+
539
+ model.buttonLinkType = buttonLinkType;
540
+ model.pageSlug = pageSlug ?? '';
541
+
542
+ return model;
543
+ });
544
+
545
+ const shouldShowButton = computed(() =>
546
+ hasValidButton({
547
+ buttonText: props.viewModel.buttonText || undefined,
548
+ buttonViewModel: props.viewModel.buttonViewModel,
549
+ }),
550
+ );
551
+
552
+ const renderText = computed(() => {
553
+ if (!props.viewModel.description) {
554
+ return '';
555
+ }
556
+
557
+ return hasRTEContent(props.viewModel.description)
558
+ ? props.viewModel.description
559
+ : getRenderText(props.viewModel.description);
560
+ });
561
+
562
+ const hasDescriptionText = computed(() => {
563
+ if (hasRTEContent(props.viewModel.description)) {
564
+ return escapeContentText(props.viewModel.description) !== '';
565
+ }
566
+
567
+ return (props.viewModel?.description?.length ?? 0) > 0;
568
+ });
569
+
570
+ const backgroundOverlay = computed<BrandPageCssStyle>(() => ({
571
+ background: props.viewModel.overlayColor?.backgroundColor,
572
+ opacity: `${props.viewModel.overlayOpacity}%`,
573
+ }));
574
+
575
+ const shouldStackOnTop = ref(false);
576
+
577
+ const onResize = (): void => {
578
+ shouldStackOnTop.value = window.innerWidth < 640;
579
+ };
580
+
581
+ onMounted(() => {
582
+ nextTick(() => {
583
+ window.addEventListener('resize', onResize);
584
+ });
585
+ });
586
+
587
+ onUnmounted(() => {
588
+ window.removeEventListener('resize', onResize);
589
+ });
255
590
  </script>
256
591
 
257
592
  <style scoped>
@@ -273,6 +608,19 @@ export default {
273
608
  margin: 0 auto;
274
609
  }
275
610
 
611
+ /*
612
+ * Full-bleed stacked image. The wrapper lives inside .c_banner-container
613
+ * (capped at --brandsite-max-width and centered) and inside the outer
614
+ * tw-px-6 padding, so a plain width:100%/-mx-6 stops at the max-width edge.
615
+ * Using the outer container-query width (100cqw = content-box inline size),
616
+ * widen to the full banner and shift left so the image spans edge to edge
617
+ * (1.5rem = tw-px-6). Text blocks stay constrained to the max-width.
618
+ */
619
+ .c_banner-stacked-image--full {
620
+ width: calc(100cqw + 3rem);
621
+ margin-inline: calc(50% - 50cqw - 1.5rem);
622
+ }
623
+
276
624
  .c_banner--with-image-flip {
277
625
  flex-direction: row-reverse;
278
626
  }
@@ -0,0 +1,201 @@
1
+ import { vi } from 'vitest';
2
+ import { mount, RouterLinkStub } from '@vue/test-utils';
3
+ import BannerSection from './BannerSection.vue';
4
+ import { createPinia, setActivePinia } from 'pinia';
5
+ import {
6
+ BRAND_PAGE_TYPES,
7
+ MEDIA_SIZES,
8
+ type BrandPageContext,
9
+ } from '@brand-page/models';
10
+ import type {
11
+ BannerSectionViewModel,
12
+ ButtonViewModel,
13
+ NavItemViewModel,
14
+ } from '@brand-page/view-models';
15
+
16
+ global.ResizeObserver = vi.fn().mockImplementation(() => ({
17
+ observe: vi.fn(),
18
+ unobserve: vi.fn(),
19
+ disconnect: vi.fn(),
20
+ }));
21
+
22
+ const LINKED_PAGE_ID = 'page-2';
23
+
24
+ const createTestNavItem = (): NavItemViewModel => ({
25
+ pageId: LINKED_PAGE_ID,
26
+ slug: 'my-brand',
27
+ displayName: 'Second Page',
28
+ routeName: 'brand-page-sub-page',
29
+ routeParams: { slug: 'my-brand', pageSlug: 'second-page' },
30
+ isRoot: false,
31
+ isHidden: false,
32
+ relativePath: 'second-page',
33
+ nestedNavItems: [],
34
+ });
35
+
36
+ const createTestBrandPageContext = (): BrandPageContext => ({
37
+ brandPageSlug: 'my-brand',
38
+ brandPageToken: '',
39
+ isMobile: false,
40
+ isPreview: true,
41
+ isProduction: false,
42
+ isServerRender: false,
43
+ options: {
44
+ mediaSizes: MEDIA_SIZES.sizeThumb,
45
+ },
46
+ isTemplate: false,
47
+ isDesignCom: false,
48
+ pageTitle: 'My Brand',
49
+ runtimeEnvironment: '',
50
+ stripePublishableKey: undefined,
51
+ customDomain: undefined,
52
+ brandPageType: BRAND_PAGE_TYPES.brandSite,
53
+ isBrandPage: false,
54
+ isBrandPageOrIsBrandContact: false,
55
+ isBrandSite: true,
56
+ isBrandContact: false,
57
+ pages: [],
58
+ isCapturingPreview: false,
59
+ recaptchaSiteKey: undefined,
60
+ nav: {
61
+ navItems: [createTestNavItem()],
62
+ },
63
+ });
64
+
65
+ const textStyle = {
66
+ fontSize: '1rem',
67
+ lineHeight: '1.5rem',
68
+ fontFamily: 'Lato',
69
+ textAlign: 'center',
70
+ overflowWrap: 'anywhere',
71
+ wordBreak: 'normal',
72
+ };
73
+
74
+ const createTestButtonViewModel = (
75
+ overrides: Partial<ButtonViewModel> = {},
76
+ ): ButtonViewModel =>
77
+ ({
78
+ id: 'button-1',
79
+ type: 'button',
80
+ role: 'banner-section',
81
+ title: 'Get Started',
82
+ url: '',
83
+ page: LINKED_PAGE_ID,
84
+ pageSlug: LINKED_PAGE_ID,
85
+ buttonLinkType: 'page',
86
+ clickActionField: 'url',
87
+ iconUrl: '',
88
+ modalImageUrl: '',
89
+ titleStyle: textStyle,
90
+ styles: { selectedButtonStyleId: 'BUTTON_STYLE_1', style: {} },
91
+ onClicked: vi.fn(),
92
+ ...overrides,
93
+ }) as ButtonViewModel;
94
+
95
+ const createTestBannerViewModel = (
96
+ overrides: Partial<BannerSectionViewModel> = {},
97
+ ): BannerSectionViewModel =>
98
+ ({
99
+ id: 'banner-1',
100
+ type: 'banner-section',
101
+ role: 'banner-section',
102
+ imageUrl: '',
103
+ title: 'Welcome',
104
+ description: 'Your brand starts here.',
105
+ buttonText: 'Get Started',
106
+ url: '',
107
+ displayStyle: 'centered',
108
+ titleStyle: textStyle,
109
+ descriptionStyle: textStyle,
110
+ buttonTextStyle: textStyle,
111
+ backgroundStyle: {},
112
+ cardStyle: {},
113
+ colorReplacements: [],
114
+ buttonPageId: LINKED_PAGE_ID,
115
+ buttonLinkType: 'page',
116
+ overlayColor: {},
117
+ overlayOpacity: 0,
118
+ buttonViewModel: createTestButtonViewModel(),
119
+ isFullWidth: true,
120
+ isBigImageType: false,
121
+ ...overrides,
122
+ }) as BannerSectionViewModel;
123
+
124
+ const mountBanner = (viewModel: BannerSectionViewModel) =>
125
+ mount(BannerSection, {
126
+ propsData: { context: createTestBrandPageContext(), viewModel },
127
+ global: {
128
+ stubs: { RouterLink: RouterLinkStub },
129
+ },
130
+ });
131
+
132
+ describe('Banner Section', () => {
133
+ beforeEach(() => {
134
+ setActivePinia(createPinia());
135
+ });
136
+
137
+ test('page-link button renders a router-link resolved by page id when the banner view model has no pageSlug', () => {
138
+ // Real banner view models never carry pageSlug; the button must still
139
+ // render via the `page` fallback and route by buttonPageId.
140
+ const wrapper = mountBanner(createTestBannerViewModel());
141
+
142
+ const link = wrapper.findComponent(RouterLinkStub);
143
+
144
+ expect(link.exists()).toBe(true);
145
+ expect(link.props('to')).toEqual({
146
+ name: 'brand-page-sub-page',
147
+ params: { slug: 'my-brand', pageSlug: 'second-page' },
148
+ });
149
+ });
150
+
151
+ test('forwards buttonLinkType and an empty pageSlug to the button view model when absent on the banner', () => {
152
+ const viewModel = createTestBannerViewModel();
153
+
154
+ mountBanner(viewModel);
155
+
156
+ expect(viewModel.buttonViewModel.buttonLinkType).toBe('page');
157
+ expect(viewModel.buttonViewModel.pageSlug).toBe('');
158
+ });
159
+
160
+ test('forwards the banner pageSlug to the button view model when present', () => {
161
+ const viewModel = createTestBannerViewModel({
162
+ pageSlug: 'second-page',
163
+ } as Partial<BannerSectionViewModel>);
164
+ const wrapper = mountBanner(viewModel);
165
+
166
+ expect(viewModel.buttonViewModel.pageSlug).toBe('second-page');
167
+ expect(wrapper.findComponent(RouterLinkStub).exists()).toBe(true);
168
+ });
169
+
170
+ test('url button renders an anchor with the url as href', () => {
171
+ const wrapper = mountBanner(
172
+ createTestBannerViewModel({
173
+ buttonLinkType: 'url',
174
+ url: 'https://example.com',
175
+ buttonViewModel: createTestButtonViewModel({
176
+ buttonLinkType: 'url',
177
+ url: 'https://example.com',
178
+ page: '',
179
+ pageSlug: '',
180
+ }),
181
+ }),
182
+ );
183
+
184
+ expect(wrapper.findComponent(RouterLinkStub).exists()).toBe(false);
185
+ const anchor = wrapper.find('a.c-button');
186
+
187
+ expect(anchor.exists()).toBe(true);
188
+ expect(anchor.attributes('href')).toBe('https://example.com');
189
+ });
190
+
191
+ test('hides the button when there is no button text', () => {
192
+ const wrapper = mountBanner(
193
+ createTestBannerViewModel({
194
+ buttonText: false,
195
+ buttonViewModel: createTestButtonViewModel({ title: '' }),
196
+ }),
197
+ );
198
+
199
+ expect(wrapper.find('.bp-button').exists()).toBe(false);
200
+ });
201
+ });
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <section data-component="booking-section">
3
3
  <div
4
- class="tw-relative tw-px-6 tw-py-8"
5
- :style="viewModel.backgroundStyle"
4
+ class="tw-relative tw-px-6"
5
+ :style="[viewModel.backgroundStyle, contentPaddingStyle]"
6
6
  style="container-type: inline-size"
7
7
  >
8
8
  <div
@@ -45,7 +45,11 @@ import type {
45
45
  ButtonViewModel,
46
46
  } from '@brand-page/view-models';
47
47
  import type { BrandPageContext } from '@brand-page/models';
48
- import { getRenderText, escapeContentText } from '@brand-page/utils';
48
+ import {
49
+ getRenderText,
50
+ escapeContentText,
51
+ contentPaddingStyle as toContentPaddingStyle,
52
+ } from '@brand-page/utils';
49
53
  import { hasRTEContent } from '@brand-page-maker/utils';
50
54
  import { hasValidButton as shouldShowButtonUtil } from '../../../utils/validator.util';
51
55
  import Button from '../button/Button.vue';
@@ -71,6 +75,10 @@ const props = defineProps({
71
75
  },
72
76
  });
73
77
 
78
+ const contentPaddingStyle = computed(() =>
79
+ toContentPaddingStyle(props.viewModel),
80
+ );
81
+
74
82
  const renderText = computed((): string => {
75
83
  if (!props.viewModel.description) {
76
84
  return '';
@@ -25,7 +25,10 @@ import {
25
25
  RECAPTCHA_ACTION_CONTACT_FORM,
26
26
  TEXT_ONLY,
27
27
  } from '../../constants';
28
- import { getRenderText } from '@brand-page/utils';
28
+ import {
29
+ getRenderText,
30
+ contentPaddingStyle as toContentPaddingStyle,
31
+ } from '@brand-page/utils';
29
32
  import { hasRTEContent } from '@brand-page-maker/utils';
30
33
  import { useReCaptcha } from '../../composables/recaptcha.composable';
31
34
 
@@ -97,6 +100,10 @@ export default defineComponent({
97
100
  return buttonTextStyle;
98
101
  },
99
102
 
103
+ contentPaddingStyle(): { paddingTop: string; paddingBottom: string } {
104
+ return toContentPaddingStyle(this.viewModel);
105
+ },
106
+
100
107
  hasCard(): boolean {
101
108
  return (
102
109
  this.viewModel.layoutStyle === ALIGN_CARD_CENTER ||
@@ -109,11 +116,10 @@ export default defineComponent({
109
116
  },
110
117
 
111
118
  hasBgColor(): boolean {
112
- return (
113
- this.viewModel.layoutStyle === ALIGN_CARD_CENTER ||
114
- this.viewModel.layoutStyle === TEXT_ONLY ||
115
- this.viewModel.layoutStyle === ALIGN_CENTERED
116
- );
119
+ // Every brandSite layout applies the background colour on the section
120
+ // root (banner-style) so the padding control reveals it; for the image
121
+ // layouts it only shows in the padding bands around the image block.
122
+ return Boolean(this.viewModel.layoutStyle);
117
123
  },
118
124
 
119
125
  isCenteredLayout(): boolean {