@designcrowd/library.brand-page 6.1.15 → 6.1.16

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 (74) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-a7cNT5AA.js → CreatedOnBrandCrowd-de-de-CmNhDMIf.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-BpZ89I9T.js → CreatedOnBrandCrowd-en-us-DgplxxWa.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-Db0Ot-mQ.js → CreatedOnBrandCrowd-es-es-DFdhFOa1.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Mbd7Hhxh.js → CreatedOnBrandCrowd-fr-ca-DjLCWBLB.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Mbd7Hhxh.js → CreatedOnBrandCrowd-fr-fr-DjLCWBLB.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-B98aoT6y.js → CreatedOnBrandCrowd-pt-br-CI2XD0OS.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-B98aoT6y.js → CreatedOnBrandCrowd-pt-pt-CI2XD0OS.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-b_GUYmhX.js → CreatedOnDcom-de-de-CkLkBztx.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-BtTIwekM.js → CreatedOnDcom-en-us-frnxBJJv.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-WbhHNEg-.js → CreatedOnDcom-es-es-CrqgiERj.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-DnQOAvS5.js → CreatedOnDcom-fr-ca-CPoEcusj.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-D7Q-aBNQ.js → CreatedOnDcom-fr-fr-BqrRH3dV.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DnqkF_ve.js → CreatedOnDcom-pt-br-CpbVi3Sw.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-DNyyHF1o.js → CreatedOnDcom-pt-pt-s0XZ3c7y.js} +1 -1
  15. package/lib/src/brand-page/models/index.d.ts +4 -1
  16. package/lib/src/brand-page/models/index.js +4 -1
  17. package/lib/src/brand-page/utils/view-model.util.js +18 -2
  18. package/lib/src/brand-page/view-models.d.ts +2 -1
  19. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +18 -0
  20. package/lib/src/{bundled-translations-BiFAlQdC.js → bundled-translations-B2q5GWTQ.js} +1 -1
  21. package/lib/src/{bundled-translations.de-DE-DuRWnOP-.js → bundled-translations.de-DE-FhH2H_rd.js} +5 -5
  22. package/lib/src/{bundled-translations.es-ES-CokHdZtW.js → bundled-translations.es-ES-_ZExxMi7.js} +5 -5
  23. package/lib/src/{bundled-translations.fr-CA-CJcMmubA.js → bundled-translations.fr-CA-Tg-ODIiC.js} +1 -1
  24. package/lib/src/{bundled-translations.fr-FR-DYPup8fJ.js → bundled-translations.fr-FR-C0sU51Mz.js} +1 -1
  25. package/lib/src/{bundled-translations.pt-BR-GSsp1kuV.js → bundled-translations.pt-BR-MYFIhA4u.js} +9 -9
  26. package/lib/src/{bundled-translations.pt-PT-DbkY8UQ3.js → bundled-translations.pt-PT-DeUBSzCB.js} +9 -9
  27. package/lib/src/bundles/bundled-translations.de-DE.json +1 -0
  28. package/lib/src/bundles/bundled-translations.es-ES.json +1 -0
  29. package/lib/src/bundles/bundled-translations.fr-CA.json +1 -0
  30. package/lib/src/bundles/bundled-translations.fr-FR.json +1 -0
  31. package/lib/src/bundles/bundled-translations.json +1 -0
  32. package/lib/src/bundles/bundled-translations.pt-BR.json +1 -0
  33. package/lib/src/bundles/bundled-translations.pt-PT.json +1 -0
  34. package/lib/src/css/library.brand-page-brandCrowd.min.css +1 -1
  35. package/lib/src/css/library.brand-page-brandPage.min.css +1 -1
  36. package/lib/src/css/library.brand-page-designCom.min.css +1 -1
  37. package/lib/src/css/library.brand-page-preview.min.css +1 -1
  38. package/lib/src/index.mjs +1 -1
  39. package/lib/src/{lib-33AmrlUy.js → lib-O_eJgJHP.js} +8465 -8415
  40. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +2 -0
  41. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +5 -0
  42. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +25 -0
  43. package/lib/src/utils/minify-brand-page-metadata.d.ts +1 -0
  44. package/lib/src/utils/minify-brand-page-metadata.js +13 -3
  45. package/package.json +1 -1
  46. package/src/brand-page/models/index.ts +9 -2
  47. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +18 -6
  48. package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +6 -2
  49. package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +12 -4
  50. package/src/brand-page/utils/view-model.util.test.ts +145 -1
  51. package/src/brand-page/utils/view-model.util.ts +27 -2
  52. package/src/brand-page/view-models.ts +2 -0
  53. package/src/bundles/bundled-translations.de-DE.json +1 -0
  54. package/src/bundles/bundled-translations.es-ES.json +1 -0
  55. package/src/bundles/bundled-translations.fr-CA.json +1 -0
  56. package/src/bundles/bundled-translations.fr-FR.json +1 -0
  57. package/src/bundles/bundled-translations.json +1 -0
  58. package/src/bundles/bundled-translations.pt-BR.json +1 -0
  59. package/src/bundles/bundled-translations.pt-PT.json +1 -0
  60. package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.test.ts +111 -0
  61. package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.vue +18 -0
  62. package/src/shared/page-settings/components/page-settings/i18n/page-settings.de-DE.json +1 -0
  63. package/src/shared/page-settings/components/page-settings/i18n/page-settings.es-ES.json +1 -0
  64. package/src/shared/page-settings/components/page-settings/i18n/page-settings.fr-CA.json +1 -0
  65. package/src/shared/page-settings/components/page-settings/i18n/page-settings.fr-FR.json +1 -0
  66. package/src/shared/page-settings/components/page-settings/i18n/page-settings.json +1 -0
  67. package/src/shared/page-settings/components/page-settings/i18n/page-settings.pt-BR.json +1 -0
  68. package/src/shared/page-settings/components/page-settings/i18n/page-settings.pt-PT.json +1 -0
  69. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +33 -0
  70. package/src/utils/minify-brand-page-metadata.test.ts +70 -1
  71. package/src/utils/minify-brand-page-metadata.ts +19 -4
  72. package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +0 -13
  73. package/lib/src/metadata-builder/builders/reorder-pages-builder.js +0 -49
  74. package/src/metadata-builder/builders/reorder-pages-builder.ts +0 -80
@@ -1277,6 +1277,7 @@ declare const _default: import("vue").DefineComponent<{}, {
1277
1277
  } | undefined;
1278
1278
  logoImageBase64Url?: string | undefined;
1279
1279
  backgroundStyle: {
1280
+ backgroundOpacity?: number | undefined;
1280
1281
  aspectRatio?: string | undefined;
1281
1282
  backgroundAttachment?: string | undefined;
1282
1283
  background?: string | undefined;
@@ -2569,6 +2570,7 @@ declare const _default: import("vue").DefineComponent<{}, {
2569
2570
  } | undefined;
2570
2571
  logoImageBase64Url?: string | undefined;
2571
2572
  backgroundStyle: {
2573
+ backgroundOpacity?: number | undefined;
2572
2574
  aspectRatio?: string | undefined;
2573
2575
  backgroundAttachment?: string | undefined;
2574
2576
  background?: string | undefined;
@@ -1235,6 +1235,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
1235
1235
  } | undefined;
1236
1236
  logoImageBase64Url?: string | undefined;
1237
1237
  backgroundStyle: {
1238
+ backgroundOpacity?: number | undefined;
1238
1239
  aspectRatio?: string | undefined;
1239
1240
  backgroundAttachment?: string | undefined;
1240
1241
  background?: string | undefined;
@@ -2527,6 +2528,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
2527
2528
  } | undefined;
2528
2529
  logoImageBase64Url?: string | undefined;
2529
2530
  backgroundStyle: {
2531
+ backgroundOpacity?: number | undefined;
2530
2532
  aspectRatio?: string | undefined;
2531
2533
  backgroundAttachment?: string | undefined;
2532
2534
  background?: string | undefined;
@@ -4638,6 +4640,8 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
4638
4640
  logoSliderMax(): number;
4639
4641
  backgroundColor(): string;
4640
4642
  isBackgroundColorEnabled(): boolean;
4643
+ backgroundOpacity(): number;
4644
+ showBackgroundOpacity(): boolean;
4641
4645
  copyrightPlaceholder(): string;
4642
4646
  copyrightText: {
4643
4647
  get(): string;
@@ -4689,6 +4693,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
4689
4693
  logoImageBase64Url?: string;
4690
4694
  }): void;
4691
4695
  onSelectColor(selectedColor: UpdateItemValuePayload<string>): void;
4696
+ onBackgroundOpacityChange(value: number | string): void;
4692
4697
  onBackgroundEnable(enabled: UpdateItemValuePayload<boolean>): void;
4693
4698
  onLogoVisibleChanged(showLogo: boolean): void;
4694
4699
  onLogoVariationSelected(selectedLogoVariationIndex: number): void;
@@ -3,6 +3,7 @@ import { PAGE_SECTION_STYLE_OPTIONS } from '@brand-page-maker/models/page-sectio
3
3
  import { PAGE_SECTIONS } from '../../models';
4
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';
5
5
  import { UI_DEVICE_MODES, UI_EVENT_SOURCES } from '../../../models';
6
+ import { isTransparentColour } from '@brand-page/utils/color.util';
6
7
  import { useUiPageSettingsStore } from '../../store';
7
8
  import { useUiNavigationStyleSettingsStore } from '../../../navigation-style-settings/store';
8
9
  import { useUiStyleSettingsStore } from '../../../style-settings/store';
@@ -141,6 +142,15 @@ export default defineComponent({
141
142
  return this.sections[this.sectionName][this.viewport]
142
143
  .backgroundColorEnabled;
143
144
  },
145
+ backgroundOpacity() {
146
+ // Real default: an un-constructed (JSON.parse'd) header never saw the constructor's default.
147
+ return (this.sections[this.sectionName][this.viewport].backgroundStyle
148
+ ?.backgroundOpacity ?? 100);
149
+ },
150
+ showBackgroundOpacity() {
151
+ // isTransparentColour('') is false, so an unset color needs its own check.
152
+ return (!!this.backgroundColor && !isTransparentColour(this.backgroundColor));
153
+ },
144
154
  copyrightPlaceholder() {
145
155
  return commonTr(`copyrightTextPlaceholder`);
146
156
  },
@@ -394,6 +404,21 @@ export default defineComponent({
394
404
  viewport: this.viewport,
395
405
  });
396
406
  },
407
+ onBackgroundOpacityChange(value) {
408
+ // Clamped here: the Slider's text field can emit an out-of-range value mid-typing.
409
+ const clamped = Math.min(100, Math.max(0, Number(value) || 0));
410
+ // A variable, not an inline literal — backgroundOpacity isn't on BrandPageCssStyle.
411
+ const backgroundStyle = {
412
+ backgroundOpacity: clamped,
413
+ };
414
+ this.pageSettingsStore.setPageSettingsSectionValue({
415
+ sectionName: this.sectionName,
416
+ propName: 'backgroundStyle',
417
+ value: backgroundStyle,
418
+ merge: true,
419
+ viewport: this.viewport,
420
+ });
421
+ },
397
422
  onBackgroundEnable(enabled) {
398
423
  this.pageSettingsStore.setPageSettingsSectionValue({
399
424
  sectionName: this.sectionName,
@@ -24,6 +24,7 @@ type MinifiedHeader = {
24
24
  logoAssetKey?: string;
25
25
  backgroundStyle?: {
26
26
  backgroundColor?: string;
27
+ backgroundOpacity?: number;
27
28
  };
28
29
  navigation?: {
29
30
  displayType?: string;
@@ -389,6 +389,18 @@ const minifyFooter = (footer, logoAssetKey) => {
389
389
  : undefined,
390
390
  };
391
391
  };
392
+ // Fully opaque (100) is the default, so it's omitted to keep the AI payload small.
393
+ const minifyHeaderBackgroundStyle = (backgroundStyle) => {
394
+ const backgroundColor = backgroundStyle?.backgroundColor;
395
+ if (!backgroundColor)
396
+ return undefined;
397
+ const backgroundOpacity = backgroundStyle.backgroundOpacity;
398
+ return {
399
+ backgroundColor,
400
+ ...(backgroundOpacity !== undefined &&
401
+ backgroundOpacity < 100 && { backgroundOpacity }),
402
+ };
403
+ };
392
404
  const minifyHeader = (header, logoAssetKey) => {
393
405
  if (!header)
394
406
  return undefined;
@@ -399,9 +411,7 @@ const minifyHeader = (header, logoAssetKey) => {
399
411
  logoImageHeight: header.logoImageHeight || undefined,
400
412
  logoImageWidth: header.logoImageWidth || undefined,
401
413
  ...(logoAssetKey && { logoAssetKey }),
402
- backgroundStyle: header.backgroundStyle?.backgroundColor
403
- ? { backgroundColor: header.backgroundStyle.backgroundColor }
404
- : undefined,
414
+ backgroundStyle: minifyHeaderBackgroundStyle(header.backgroundStyle),
405
415
  navigation: nav
406
416
  ? {
407
417
  displayType: nav.displayType || 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.1.15",
4
+ "version": "6.1.16",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -90,6 +90,10 @@ export type BackgroundStyle = Partial<
90
90
  Omit<BrandPageCssStyle, 'backgroundColor'>
91
91
  >;
92
92
 
93
+ export interface BackgroundColorStyle extends BrandPageCssStyle {
94
+ backgroundOpacity?: number;
95
+ }
96
+
93
97
  /**
94
98
  * The complete default for a page's background media overlay. Seeded into
95
99
  * metadata (constructor for new models, migration-20.5 for persisted partials) so
@@ -734,7 +738,7 @@ export abstract class AbstractPageHeaderStyles {
734
738
  logoImage: LogoImageModel;
735
739
  logoTemplateImage: LogoTemplateImageModel | undefined;
736
740
  logoImageBase64Url?: string;
737
- backgroundStyle: BrandPageCssStyle;
741
+ backgroundStyle: BackgroundColorStyle;
738
742
  style: BrandPageCssStyle;
739
743
  viewport: ViewportsType;
740
744
 
@@ -771,7 +775,10 @@ export abstract class AbstractPageHeaderStyles {
771
775
  this.logoVariation = options.logoVariation || 0;
772
776
 
773
777
  this.style = options.style || {};
774
- this.backgroundStyle = options.backgroundStyle || {};
778
+ this.backgroundStyle = {
779
+ ...(options.backgroundStyle || {}),
780
+ backgroundOpacity: options.backgroundStyle?.backgroundOpacity ?? 100,
781
+ };
775
782
  }
776
783
  }
777
784
 
@@ -152,7 +152,9 @@ exports[`migration.util > Banner Contents - text Align and Font Family should no
152
152
  "viewport": "mobile",
153
153
  },
154
154
  "pageHeaderDesktop": PageHeaderDesktop {
155
- "backgroundStyle": {},
155
+ "backgroundStyle": {
156
+ "backgroundOpacity": 100,
157
+ },
156
158
  "displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
157
159
  "isLogoOptional": false,
158
160
  "isLogoVisible": true,
@@ -225,7 +227,9 @@ exports[`migration.util > Banner Contents - text Align and Font Family should no
225
227
  "viewport": "desktop",
226
228
  },
227
229
  "pageHeaderMobile": PageHeaderMobile {
228
- "backgroundStyle": {},
230
+ "backgroundStyle": {
231
+ "backgroundOpacity": 100,
232
+ },
229
233
  "displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
230
234
  "isLogoOptional": false,
231
235
  "isLogoVisible": true,
@@ -645,7 +649,9 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
645
649
  "viewport": "mobile",
646
650
  },
647
651
  "pageHeaderDesktop": PageHeaderDesktop {
648
- "backgroundStyle": {},
652
+ "backgroundStyle": {
653
+ "backgroundOpacity": 100,
654
+ },
649
655
  "displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
650
656
  "isLogoOptional": false,
651
657
  "isLogoVisible": true,
@@ -718,7 +724,9 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
718
724
  "viewport": "desktop",
719
725
  },
720
726
  "pageHeaderMobile": PageHeaderMobile {
721
- "backgroundStyle": {},
727
+ "backgroundStyle": {
728
+ "backgroundOpacity": 100,
729
+ },
722
730
  "displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
723
731
  "isLogoOptional": false,
724
732
  "isLogoVisible": true,
@@ -1122,7 +1130,9 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
1122
1130
  "viewport": "mobile",
1123
1131
  },
1124
1132
  "pageHeaderDesktop": PageHeaderDesktop {
1125
- "backgroundStyle": {},
1133
+ "backgroundStyle": {
1134
+ "backgroundOpacity": 100,
1135
+ },
1126
1136
  "displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
1127
1137
  "isLogoOptional": false,
1128
1138
  "isLogoVisible": true,
@@ -1195,7 +1205,9 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
1195
1205
  "viewport": "desktop",
1196
1206
  },
1197
1207
  "pageHeaderMobile": PageHeaderMobile {
1198
- "backgroundStyle": {},
1208
+ "backgroundStyle": {
1209
+ "backgroundOpacity": 100,
1210
+ },
1199
1211
  "displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
1200
1212
  "isLogoOptional": false,
1201
1213
  "isLogoVisible": true,
@@ -137,7 +137,9 @@ exports[`migration02.ts > migration02 should return converted letterspacing 1`]
137
137
  "viewport": "mobile",
138
138
  },
139
139
  "pageHeaderDesktop": PageHeaderDesktop {
140
- "backgroundStyle": {},
140
+ "backgroundStyle": {
141
+ "backgroundOpacity": 100,
142
+ },
141
143
  "displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
142
144
  "isLogoOptional": false,
143
145
  "isLogoVisible": true,
@@ -211,7 +213,9 @@ exports[`migration02.ts > migration02 should return converted letterspacing 1`]
211
213
  "viewport": "desktop",
212
214
  },
213
215
  "pageHeaderMobile": PageHeaderMobile {
214
- "backgroundStyle": {},
216
+ "backgroundStyle": {
217
+ "backgroundOpacity": 100,
218
+ },
215
219
  "displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
216
220
  "isLogoOptional": false,
217
221
  "isLogoVisible": true,
@@ -136,7 +136,9 @@ BrandPageModel {
136
136
  "viewport": "mobile",
137
137
  },
138
138
  "pageHeaderDesktop": PageHeaderDesktop {
139
- "backgroundStyle": {},
139
+ "backgroundStyle": {
140
+ "backgroundOpacity": 100,
141
+ },
140
142
  "displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
141
143
  "isLogoOptional": false,
142
144
  "isLogoVisible": true,
@@ -210,7 +212,9 @@ BrandPageModel {
210
212
  "viewport": "desktop",
211
213
  },
212
214
  "pageHeaderMobile": PageHeaderMobile {
213
- "backgroundStyle": {},
215
+ "backgroundStyle": {
216
+ "backgroundOpacity": 100,
217
+ },
214
218
  "displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
215
219
  "isLogoOptional": false,
216
220
  "isLogoVisible": true,
@@ -593,7 +597,9 @@ BrandPageModel {
593
597
  "viewport": "mobile",
594
598
  },
595
599
  "pageHeaderDesktop": PageHeaderDesktop {
596
- "backgroundStyle": {},
600
+ "backgroundStyle": {
601
+ "backgroundOpacity": 100,
602
+ },
597
603
  "displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
598
604
  "isLogoOptional": false,
599
605
  "isLogoVisible": true,
@@ -667,7 +673,9 @@ BrandPageModel {
667
673
  "viewport": "desktop",
668
674
  },
669
675
  "pageHeaderMobile": PageHeaderMobile {
670
- "backgroundStyle": {},
676
+ "backgroundStyle": {
677
+ "backgroundOpacity": 100,
678
+ },
671
679
  "displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
672
680
  "isLogoOptional": false,
673
681
  "isLogoVisible": true,
@@ -6,12 +6,13 @@ import {
6
6
  type ContactFormContent,
7
7
  type ColorItem,
8
8
  } from '../models';
9
- import { BRAND_PAGE_TYPES, PageStyles } from '../models';
9
+ import { BRAND_PAGE_TYPES, PageHeaderDesktop, PageStyles } from '../models';
10
10
  import {
11
11
  mapToBannerSectionViewModel,
12
12
  mapToContactFormViewModel,
13
13
  mapToImageViewModel,
14
14
  mapToMailingListViewModel,
15
+ mapToPageHeaderViewModel,
15
16
  mapToProfileViewModel,
16
17
  mapToTextViewModel,
17
18
  mapToViewModel,
@@ -508,6 +509,149 @@ describe('view model util', () => {
508
509
  expect(url.searchParams.has('w')).toBe(true);
509
510
  expect(wParamValue).toBe('1080');
510
511
  });
512
+
513
+ describe('mapToPageHeaderViewModel background opacity (BP-5851)', () => {
514
+ const mapHeader = (header: PageHeaderDesktop) =>
515
+ mapToPageHeaderViewModel(header, new PageStyles(), []);
516
+
517
+ test('constructing without an explicit backgroundOpacity defaults it to 100 and renders unchanged', () => {
518
+ const header = new PageHeaderDesktop({
519
+ backgroundStyle: { backgroundColor: '#ff0000' },
520
+ });
521
+
522
+ expect(header.backgroundStyle.backgroundOpacity).toBe(100);
523
+
524
+ const viewModel = mapHeader(header);
525
+
526
+ expect(viewModel.backgroundStyle).toEqual({ backgroundColor: '#ff0000' });
527
+ });
528
+
529
+ test('header constructed with no options still gets a concrete backgroundOpacity', () => {
530
+ expect(new PageHeaderDesktop().backgroundStyle.backgroundOpacity).toBe(
531
+ 100,
532
+ );
533
+ });
534
+
535
+ test('un-constructed legacy header (parsed metadata that never ran the constructor) renders fully opaque', () => {
536
+ // The maker's live-preview path can hold the header JSON.parse'd verbatim.
537
+ const header = JSON.parse(
538
+ JSON.stringify(
539
+ new PageHeaderDesktop({
540
+ backgroundStyle: { backgroundColor: '#ff0000' },
541
+ }),
542
+ ),
543
+ ) as PageHeaderDesktop;
544
+
545
+ delete header.backgroundStyle.backgroundOpacity;
546
+
547
+ const viewModel = mapHeader(header);
548
+
549
+ expect(viewModel.backgroundStyle).toEqual({ backgroundColor: '#ff0000' });
550
+ });
551
+
552
+ test('a header with backgroundStyle itself absent renders fully opaque instead of throwing', () => {
553
+ const header = new PageHeaderDesktop();
554
+
555
+ delete (header as { backgroundStyle?: unknown }).backgroundStyle;
556
+
557
+ expect(() => mapHeader(header)).not.toThrow();
558
+ expect(mapHeader(header).backgroundStyle).toEqual({});
559
+ });
560
+
561
+ test('opacity 100 (default) leaves backgroundColor unchanged', () => {
562
+ const header = new PageHeaderDesktop({
563
+ backgroundStyle: { backgroundColor: '#ff0000', backgroundOpacity: 100 },
564
+ });
565
+
566
+ const viewModel = mapHeader(header);
567
+
568
+ expect(viewModel.backgroundStyle.backgroundColor).toBe('#ff0000');
569
+ });
570
+
571
+ test('opacity 60 composites into the expected 8-digit hex backgroundColor', () => {
572
+ const header = new PageHeaderDesktop({
573
+ backgroundStyle: { backgroundColor: '#ff0000', backgroundOpacity: 60 },
574
+ });
575
+
576
+ const viewModel = mapHeader(header);
577
+
578
+ expect(viewModel.backgroundStyle.backgroundColor).toBe('#ff000099');
579
+ });
580
+
581
+ test('opacity below 100 composites into an 8-digit hex backgroundColor', () => {
582
+ const header = new PageHeaderDesktop({
583
+ backgroundStyle: { backgroundColor: '#ff0000', backgroundOpacity: 50 },
584
+ });
585
+
586
+ const viewModel = mapHeader(header);
587
+
588
+ expect(viewModel.backgroundStyle.backgroundColor).toBe('#ff000080');
589
+ });
590
+
591
+ test('opacity 0 composites to the transparent keyword', () => {
592
+ const header = new PageHeaderDesktop({
593
+ backgroundStyle: { backgroundColor: '#ff0000', backgroundOpacity: 0 },
594
+ });
595
+
596
+ const viewModel = mapHeader(header);
597
+
598
+ expect(viewModel.backgroundStyle.backgroundColor).toBe('transparent');
599
+ });
600
+
601
+ test('a palette-matched (var(--palette-n)) backgroundColor is faded via color-mix, not handed back unfaded', () => {
602
+ const header = new PageHeaderDesktop({
603
+ backgroundStyle: {
604
+ backgroundColor: 'var(--palette-1)',
605
+ backgroundOpacity: 30,
606
+ },
607
+ });
608
+
609
+ const viewModel = mapHeader(header);
610
+
611
+ expect(viewModel.backgroundStyle.backgroundColor).toBe(
612
+ 'color-mix(in srgb, var(--palette-1) 30%, transparent)',
613
+ );
614
+ });
615
+
616
+ test('transparent backgroundColor is left unchanged regardless of opacity', () => {
617
+ const header = new PageHeaderDesktop({
618
+ backgroundStyle: {
619
+ backgroundColor: 'transparent',
620
+ backgroundOpacity: 40,
621
+ },
622
+ });
623
+
624
+ const viewModel = mapHeader(header);
625
+
626
+ expect(viewModel.backgroundStyle.backgroundColor).toBe('transparent');
627
+ });
628
+
629
+ test('unset backgroundColor is left unchanged regardless of opacity', () => {
630
+ const header = new PageHeaderDesktop({
631
+ backgroundStyle: { backgroundOpacity: 40 },
632
+ });
633
+
634
+ const viewModel = mapHeader(header);
635
+
636
+ expect(viewModel.backgroundStyle.backgroundColor).toBeUndefined();
637
+ });
638
+
639
+ test.each([100, 30])(
640
+ 'backgroundOpacity %i never leaks into the rendered style, and CSS opacity is never set (children must stay opaque)',
641
+ (backgroundOpacity) => {
642
+ const header = new PageHeaderDesktop({
643
+ backgroundStyle: { backgroundColor: '#ff0000', backgroundOpacity },
644
+ });
645
+
646
+ const viewModel = mapHeader(header);
647
+
648
+ expect(viewModel.backgroundStyle).not.toHaveProperty(
649
+ 'backgroundOpacity',
650
+ );
651
+ expect(viewModel.backgroundStyle.opacity).toBeUndefined();
652
+ },
653
+ );
654
+ });
511
655
  });
512
656
 
513
657
  // The reCAPTCHA footnote colour is a raw colour anchored to the surface it sits on.
@@ -88,9 +88,10 @@ import type {
88
88
  NavItemViewModel,
89
89
  } from '../view-models';
90
90
  import {
91
+ backgroundColorFromStyle,
91
92
  isTransparentColour,
92
93
  mutedTextForBackground,
93
- backgroundColorFromStyle,
94
+ withColorOpacity,
94
95
  } from './color.util';
95
96
  import { resolveColorReference } from './color-palette.util';
96
97
  import {
@@ -1281,7 +1282,31 @@ export const mapToPageHeaderViewModel = (
1281
1282
  hamburgerMenuStyle,
1282
1283
  }),
1283
1284
  style: pageHeader.style,
1284
- backgroundStyle: pageHeader.backgroundStyle,
1285
+ backgroundStyle: getHeaderBackgroundStyleWithOpacity(pageHeader),
1286
+ };
1287
+ };
1288
+
1289
+ // Folds backgroundOpacity into backgroundColor's alpha so only the container's paint fades, not its children.
1290
+ const getHeaderBackgroundStyleWithOpacity = (
1291
+ pageHeader: PageHeaderDesktop | PageHeaderMobile,
1292
+ ): BrandPageCssStyle => {
1293
+ // Real defaults: an un-constructed maker header, or metadata older than migration-11's backfill, can lack these entirely.
1294
+ const { backgroundOpacity = 100, ...backgroundStyle } =
1295
+ pageHeader.backgroundStyle ?? {};
1296
+ const { backgroundColor } = backgroundStyle;
1297
+
1298
+ if (
1299
+ backgroundOpacity >= 100 ||
1300
+ !backgroundColor ||
1301
+ isTransparentColour(backgroundColor)
1302
+ ) {
1303
+ return backgroundStyle;
1304
+ }
1305
+
1306
+ // withColorOpacity, not setRgbAlpha: fades a var(--palette-n) reference too.
1307
+ return {
1308
+ ...backgroundStyle,
1309
+ backgroundColor: withColorOpacity(backgroundColor, backgroundOpacity / 100),
1285
1310
  };
1286
1311
  };
1287
1312
 
@@ -259,9 +259,11 @@ export interface PageHeaderViewModel extends Omit<
259
259
  | 'logoTemplateImage'
260
260
  | 'logoImageHeight'
261
261
  | 'logoImageWidth'
262
+ | 'backgroundStyle'
262
263
  > {
263
264
  logo: LogoViewModel;
264
265
  pages: PageViewModel[];
266
+ backgroundStyle: BrandPageCssStyle;
265
267
  }
266
268
 
267
269
  export interface PageFooterViewModel extends Omit<
@@ -1841,6 +1841,7 @@
1841
1841
  "headerSettings": "Kopfzeilen-Einstellungen",
1842
1842
  "navigationSectionTitle": "Navigation",
1843
1843
  "headerSectionTitle": "Header",
1844
+ "backgroundOpacity": "Hintergrunddeckkraft",
1844
1845
  "buttonSectionTitle": "Schaltfläche",
1845
1846
  "footerSettings": "Fußzeilen-Einstellungen",
1846
1847
  "managePages": "Seiten verwalten",
@@ -1841,6 +1841,7 @@
1841
1841
  "headerSettings": "Configuración del encabezado",
1842
1842
  "navigationSectionTitle": "Navegación",
1843
1843
  "headerSectionTitle": "Encabezado",
1844
+ "backgroundOpacity": "Opacidad del fondo",
1844
1845
  "buttonSectionTitle": "Botón",
1845
1846
  "footerSettings": "Configuración del pie de página",
1846
1847
  "managePages": "Gestionar páginas",
@@ -1841,6 +1841,7 @@
1841
1841
  "headerSettings": "Paramètres d'en-tête",
1842
1842
  "navigationSectionTitle": "Navigation",
1843
1843
  "headerSectionTitle": "En-tête",
1844
+ "backgroundOpacity": "Opacité de l'arrière-plan",
1844
1845
  "buttonSectionTitle": "Bouton",
1845
1846
  "footerSettings": "Paramètres de pied de page",
1846
1847
  "managePages": "Gérer les pages",
@@ -1841,6 +1841,7 @@
1841
1841
  "headerSettings": "Paramètres d'en-tête",
1842
1842
  "navigationSectionTitle": "Navigation",
1843
1843
  "headerSectionTitle": "En-tête",
1844
+ "backgroundOpacity": "Opacité de l'arrière-plan",
1844
1845
  "buttonSectionTitle": "Bouton",
1845
1846
  "footerSettings": "Paramètres de pied de page",
1846
1847
  "managePages": "Gérer les pages",
@@ -1841,6 +1841,7 @@
1841
1841
  "headerSettings": "Header Settings",
1842
1842
  "navigationSectionTitle": "Navigation",
1843
1843
  "headerSectionTitle": "Header",
1844
+ "backgroundOpacity": "Background opacity",
1844
1845
  "buttonSectionTitle": "Button",
1845
1846
  "footerSettings": "Footer Settings",
1846
1847
  "managePages": "Manage Pages",
@@ -1841,6 +1841,7 @@
1841
1841
  "headerSettings": "Configurações do cabeçalho",
1842
1842
  "navigationSectionTitle": "Navegação",
1843
1843
  "headerSectionTitle": "Cabeçalho",
1844
+ "backgroundOpacity": "Opacidade do plano de fundo",
1844
1845
  "buttonSectionTitle": "Botão",
1845
1846
  "footerSettings": "Definições de rodapé",
1846
1847
  "managePages": "Gerenciar páginas",
@@ -1841,6 +1841,7 @@
1841
1841
  "headerSettings": "Definições de cabeçalho",
1842
1842
  "navigationSectionTitle": "Navegação",
1843
1843
  "headerSectionTitle": "Cabeçalho",
1844
+ "backgroundOpacity": "Opacidade do fundo",
1844
1845
  "buttonSectionTitle": "Botão",
1845
1846
  "footerSettings": "Definições de rodapé",
1846
1847
  "managePages": "Gerir páginas",