@designcrowd/library.brand-page 6.1.14 → 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 (93) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-CNqGW-GB.js → CreatedOnBrandCrowd-de-de-CmNhDMIf.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-BWZeor_R.js → CreatedOnBrandCrowd-en-us-DgplxxWa.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-D_-5Qdpd.js → CreatedOnBrandCrowd-es-es-DFdhFOa1.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-BC6Xo2uA.js → CreatedOnBrandCrowd-fr-ca-DjLCWBLB.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-BC6Xo2uA.js → CreatedOnBrandCrowd-fr-fr-DjLCWBLB.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-D10DwNv3.js → CreatedOnBrandCrowd-pt-br-CI2XD0OS.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-D10DwNv3.js → CreatedOnBrandCrowd-pt-pt-CI2XD0OS.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-BDHrHOOB.js → CreatedOnDcom-de-de-CkLkBztx.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-EbniAwLE.js → CreatedOnDcom-en-us-frnxBJJv.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-BpAIMuUH.js → CreatedOnDcom-es-es-CrqgiERj.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BDSu4efU.js → CreatedOnDcom-fr-ca-CPoEcusj.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-BR7f1SWB.js → CreatedOnDcom-fr-fr-BqrRH3dV.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DzY1XBcr.js → CreatedOnDcom-pt-br-CpbVi3Sw.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-1v5N9vgW.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/components/brand-page-maker/BrandPageMaker.mixin.js +4 -1
  20. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +3 -0
  21. package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +15 -0
  22. package/lib/src/brand-page-maker/composables/useEventTracker.js +36 -0
  23. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +18 -0
  24. package/lib/src/{bundled-translations-BiFAlQdC.js → bundled-translations-B2q5GWTQ.js} +1 -1
  25. package/lib/src/{bundled-translations.de-DE-DuRWnOP-.js → bundled-translations.de-DE-FhH2H_rd.js} +5 -5
  26. package/lib/src/{bundled-translations.es-ES-CokHdZtW.js → bundled-translations.es-ES-_ZExxMi7.js} +5 -5
  27. package/lib/src/{bundled-translations.fr-CA-CJcMmubA.js → bundled-translations.fr-CA-Tg-ODIiC.js} +1 -1
  28. package/lib/src/{bundled-translations.fr-FR-DYPup8fJ.js → bundled-translations.fr-FR-C0sU51Mz.js} +1 -1
  29. package/lib/src/{bundled-translations.pt-BR-GSsp1kuV.js → bundled-translations.pt-BR-MYFIhA4u.js} +9 -9
  30. package/lib/src/{bundled-translations.pt-PT-DbkY8UQ3.js → bundled-translations.pt-PT-DeUBSzCB.js} +9 -9
  31. package/lib/src/bundles/bundled-translations.de-DE.json +1 -0
  32. package/lib/src/bundles/bundled-translations.es-ES.json +1 -0
  33. package/lib/src/bundles/bundled-translations.fr-CA.json +1 -0
  34. package/lib/src/bundles/bundled-translations.fr-FR.json +1 -0
  35. package/lib/src/bundles/bundled-translations.json +1 -0
  36. package/lib/src/bundles/bundled-translations.pt-BR.json +1 -0
  37. package/lib/src/bundles/bundled-translations.pt-PT.json +1 -0
  38. package/lib/src/css/library.brand-page-brandCrowd.min.css +1 -1
  39. package/lib/src/css/library.brand-page-brandPage.min.css +1 -1
  40. package/lib/src/css/library.brand-page-designCom.min.css +1 -1
  41. package/lib/src/css/library.brand-page-preview.min.css +1 -1
  42. package/lib/src/index.mjs +2 -2
  43. package/lib/src/{lib-BMP5cseh.js → lib-O_eJgJHP.js} +16498 -16437
  44. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +3 -0
  45. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +3 -0
  46. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +3 -0
  47. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +3 -0
  48. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +5 -0
  49. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +5 -0
  50. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +25 -0
  51. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +3 -0
  52. package/lib/src/shared/side-bar/components/SideBarTabButton.mixin.d.ts +3 -0
  53. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +3 -0
  54. package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +3 -0
  55. package/lib/src/tracker.d.ts +1 -1
  56. package/lib/src/tracker.js +1 -1
  57. package/lib/src/tracker.mjs +9 -7
  58. package/lib/src/{useEventTracker-Dmc2jFdN.js → useEventTracker-C5Ql5p7u.js} +56 -22
  59. package/lib/src/utils/minify-brand-page-metadata.d.ts +1 -0
  60. package/lib/src/utils/minify-brand-page-metadata.js +13 -3
  61. package/package.json +1 -1
  62. package/src/brand-page/models/index.ts +9 -2
  63. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +18 -6
  64. package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +6 -2
  65. package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +12 -4
  66. package/src/brand-page/utils/view-model.util.test.ts +145 -1
  67. package/src/brand-page/utils/view-model.util.ts +27 -2
  68. package/src/brand-page/view-models.ts +2 -0
  69. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +9 -1
  70. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.preview-gate.test.ts +38 -1
  71. package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.test.ts +123 -0
  72. package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.vue +25 -0
  73. package/src/brand-page-maker/composables/useEventTracker.ts +41 -0
  74. package/src/bundles/bundled-translations.de-DE.json +1 -0
  75. package/src/bundles/bundled-translations.es-ES.json +1 -0
  76. package/src/bundles/bundled-translations.fr-CA.json +1 -0
  77. package/src/bundles/bundled-translations.fr-FR.json +1 -0
  78. package/src/bundles/bundled-translations.json +1 -0
  79. package/src/bundles/bundled-translations.pt-BR.json +1 -0
  80. package/src/bundles/bundled-translations.pt-PT.json +1 -0
  81. package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.test.ts +111 -0
  82. package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.vue +18 -0
  83. package/src/shared/page-settings/components/page-settings/i18n/page-settings.de-DE.json +1 -0
  84. package/src/shared/page-settings/components/page-settings/i18n/page-settings.es-ES.json +1 -0
  85. package/src/shared/page-settings/components/page-settings/i18n/page-settings.fr-CA.json +1 -0
  86. package/src/shared/page-settings/components/page-settings/i18n/page-settings.fr-FR.json +1 -0
  87. package/src/shared/page-settings/components/page-settings/i18n/page-settings.json +1 -0
  88. package/src/shared/page-settings/components/page-settings/i18n/page-settings.pt-BR.json +1 -0
  89. package/src/shared/page-settings/components/page-settings/i18n/page-settings.pt-PT.json +1 -0
  90. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +33 -0
  91. package/src/tracker.ts +2 -0
  92. package/src/utils/minify-brand-page-metadata.test.ts +70 -1
  93. package/src/utils/minify-brand-page-metadata.ts +19 -4
@@ -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<
@@ -48,7 +48,11 @@ import AssetLibraryClient from '../../../clients/asset-library-clients';
48
48
  import { useShopConfigStore } from '@shared/ecommerce-settings/store/shop-settings-store';
49
49
  import type { ShopConfigData } from '@brand-page/models/shop.model';
50
50
  import { useProductConfigStore } from '@brand-page-maker/stores/product-config/product-config-store';
51
- import { UPGRADE_CTA_LOCATIONS } from '../../composables/useEventTracker';
51
+ import {
52
+ useEventTracker,
53
+ UPGRADE_CTA_LOCATIONS,
54
+ PREVIEW_CTA_LOCATIONS,
55
+ } from '../../composables/useEventTracker';
52
56
  import { commonTr } from '@src/composables/useTranslate';
53
57
  import { useSnapshot } from '../../../composables/useSnapshot';
54
58
  import { usePageNavigationStore } from '@src/shared/page-navigation/store/page-navigation-store';
@@ -460,6 +464,10 @@ export default defineComponent({
460
464
  this.$emit(EVENTS.Back);
461
465
  },
462
466
  onPreview() {
467
+ // Every click is counted, before the gating below — the modal-open event
468
+ // fires separately, so click → open reads as a funnel.
469
+ useEventTracker().sendPreviewClickEvent(PREVIEW_CTA_LOCATIONS.MAKER);
470
+
463
471
  // The preview frames a SAVED, owned draft. A signed-out customer can't have
464
472
  // one, and the save needs an owner — so sign-in comes first, via the host's
465
473
  // Save Progress flow.
@@ -1,6 +1,11 @@
1
- import { describe, test, expect, vi } from 'vitest';
1
+ import { describe, test, expect, vi, beforeEach } from 'vitest';
2
2
  import BrandPageMakerMixin from './BrandPageMaker.mixin';
3
3
  import { EVENTS } from '../../../constants/brand-page-maker';
4
+ import {
5
+ useEventTracker,
6
+ EVENTS as TRACKER_EVENTS,
7
+ PREVIEW_CTA_LOCATIONS,
8
+ } from '@brand-page-maker/composables/useEventTracker';
4
9
 
5
10
  // The preview frames a SAVED, owned draft, so who saves it decides when the modal
6
11
  // may open. Signed out: nobody owns a draft, so hand off to the host's Save Progress
@@ -68,6 +73,38 @@ describe('BrandPageMaker.onPreview', () => {
68
73
  });
69
74
  });
70
75
 
76
+ describe('BrandPageMaker.onPreview click analytics', () => {
77
+ const received: { name: string; props: any }[] = [];
78
+
79
+ useEventTracker().subscribe((event) =>
80
+ received.push({ name: event.eventName, props: event.eventProps }),
81
+ );
82
+
83
+ beforeEach(() => {
84
+ received.length = 0;
85
+ });
86
+
87
+ // The click is counted before the sign-in/save gating, so every branch —
88
+ // including ones where the modal never opens — reports the same event.
89
+ test.each([
90
+ ['signed out', { isUserAuthenticated: false }],
91
+ [
92
+ 'pre-purchase',
93
+ { containerStore: { isPrePurchase: true, livePreviewUrl: undefined } },
94
+ ],
95
+ ['post-purchase', {}],
96
+ ])('every click is counted: %s', (_, overrides) => {
97
+ onPreview.call(createContext(overrides as any));
98
+
99
+ expect(received).toEqual([
100
+ {
101
+ name: TRACKER_EVENTS.WEBSITE_MAKER_LIVE_PREVIEW_CLICK,
102
+ props: { cta_location: PREVIEW_CTA_LOCATIONS.MAKER },
103
+ },
104
+ ]);
105
+ });
106
+ });
107
+
71
108
  describe('BrandPageMaker livePreviewUrl watcher', () => {
72
109
  test('opens the preview once the host save hands back a URL', () => {
73
110
  const context = createContext({
@@ -7,6 +7,12 @@ import {
7
7
  useBrandPageContainerStore,
8
8
  useBrandPageMakerStore,
9
9
  } from '@brand-page-maker/stores';
10
+ import {
11
+ useEventTracker,
12
+ EVENTS,
13
+ PREVIEW_CTA_LOCATIONS,
14
+ PREVIEW_MODAL_CONTINUATION_EVENTS,
15
+ } from '@brand-page-maker/composables/useEventTracker';
10
16
 
11
17
  const PREVIEW_URL = 'https://brand.site/_p/request?token=signed';
12
18
 
@@ -243,4 +249,121 @@ describe('PreviewModalWithState', () => {
243
249
 
244
250
  expect(wrapper.emitted('edit')).toHaveLength(1);
245
251
  });
252
+
253
+ describe('analytics', () => {
254
+ const received: { name: string; props: any }[] = [];
255
+
256
+ useEventTracker().subscribe((event) =>
257
+ received.push({ name: event.eventName, props: event.eventProps }),
258
+ );
259
+
260
+ const openModal = async (props: Record<string, unknown> = {}) => {
261
+ vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
262
+ saved: true,
263
+ canPublish: true,
264
+ } as any);
265
+ vi.spyOn(containerStore, 'showSaveSuccessMessage').mockResolvedValue(
266
+ undefined,
267
+ );
268
+ vi.spyOn(
269
+ useBrandPageMakerStore(),
270
+ 'showNextNotificationMessage',
271
+ ).mockResolvedValue(undefined);
272
+
273
+ const wrapper = shallowMount(PreviewModalWithState, {
274
+ props: { visible: false, ...props },
275
+ });
276
+
277
+ await wrapper.setProps({ visible: true });
278
+ await flushPromises();
279
+
280
+ return wrapper;
281
+ };
282
+
283
+ beforeEach(() => {
284
+ received.length = 0;
285
+ });
286
+
287
+ test('opening fires the modal event, stamped with the editor surface by default', async () => {
288
+ await openModal();
289
+
290
+ expect(received).toEqual([
291
+ {
292
+ name: EVENTS.WEBSITE_MAKER_LIVE_PREVIEW_MODAL,
293
+ props: {
294
+ cta_location: PREVIEW_CTA_LOCATIONS.MAKER,
295
+ continuation: null,
296
+ },
297
+ },
298
+ ]);
299
+ });
300
+
301
+ test('the hosting surface stamps its own cta_location', async () => {
302
+ await openModal({ ctaLocation: PREVIEW_CTA_LOCATIONS.MANAGE_WEBSITE });
303
+
304
+ expect(received).toEqual([
305
+ {
306
+ name: EVENTS.WEBSITE_MAKER_LIVE_PREVIEW_MODAL,
307
+ props: {
308
+ cta_location: PREVIEW_CTA_LOCATIONS.MANAGE_WEBSITE,
309
+ continuation: null,
310
+ },
311
+ },
312
+ ]);
313
+ });
314
+
315
+ test.each([
316
+ ['close', PREVIEW_MODAL_CONTINUATION_EVENTS.EXIT],
317
+ // On the editor (cta=publish, the default) the button reads "Back to Editor".
318
+ ['edit', PREVIEW_MODAL_CONTINUATION_EVENTS.BACK_TO_EDITOR],
319
+ ['publish', PREVIEW_MODAL_CONTINUATION_EVENTS.PUBLISH],
320
+ ])('%s continues the modal event as %s', async (emitName, continuation) => {
321
+ const wrapper = await openModal();
322
+
323
+ received.length = 0;
324
+ await wrapper.findComponent(LivePreviewModal).vm.$emit(emitName);
325
+ await flushPromises();
326
+
327
+ expect(received).toEqual([
328
+ {
329
+ name: EVENTS.WEBSITE_MAKER_LIVE_PREVIEW_MODAL,
330
+ props: { cta_location: PREVIEW_CTA_LOCATIONS.MAKER, continuation },
331
+ },
332
+ ]);
333
+ });
334
+
335
+ // The manage page's button reads "Edit" (cta=edit), so its continuation is
336
+ // edit — the editor's back_to_editor case is covered above.
337
+ test('continuations carry the hosting surface and its label', async () => {
338
+ const wrapper = await openModal({
339
+ cta: 'edit',
340
+ ctaLocation: PREVIEW_CTA_LOCATIONS.MANAGE_WEBSITE,
341
+ });
342
+
343
+ received.length = 0;
344
+ await wrapper.findComponent(LivePreviewModal).vm.$emit('edit');
345
+ await flushPromises();
346
+
347
+ expect(received).toEqual([
348
+ {
349
+ name: EVENTS.WEBSITE_MAKER_LIVE_PREVIEW_MODAL,
350
+ props: {
351
+ cta_location: PREVIEW_CTA_LOCATIONS.MANAGE_WEBSITE,
352
+ continuation: PREVIEW_MODAL_CONTINUATION_EVENTS.EDIT,
353
+ },
354
+ },
355
+ ]);
356
+ });
357
+
358
+ // Retry re-runs the save, it isn't a decision about the preview — not tracked.
359
+ test('retry is not tracked', async () => {
360
+ const wrapper = await openModal();
361
+
362
+ received.length = 0;
363
+ await wrapper.findComponent(LivePreviewModal).vm.$emit('retry');
364
+ await flushPromises();
365
+
366
+ expect(received).toEqual([]);
367
+ });
368
+ });
246
369
  });
@@ -27,18 +27,26 @@ import {
27
27
  useBrandPageContainerStore,
28
28
  useBrandPageMakerStore,
29
29
  } from '@brand-page-maker/stores';
30
+ import {
31
+ useEventTracker,
32
+ PREVIEW_CTA_LOCATIONS,
33
+ PREVIEW_MODAL_CONTINUATION_EVENTS,
34
+ } from '@brand-page-maker/composables/useEventTracker';
30
35
  import type { UiDeviceMode } from '@shared/models';
31
36
 
32
37
  const props = defineProps({
33
38
  visible: { type: Boolean, default: false },
34
39
  // Per-surface header CTA; defaults to 'publish' so the editor is unchanged.
35
40
  cta: { type: String as PropType<LivePreviewCta>, default: 'publish' },
41
+ // Which surface hosts the preview — stamped on its analytics as cta_location.
42
+ ctaLocation: { type: String, default: PREVIEW_CTA_LOCATIONS.MAKER },
36
43
  });
37
44
 
38
45
  const emit = defineEmits<{ close: []; publish: []; edit: [] }>();
39
46
 
40
47
  const containerStore = useBrandPageContainerStore();
41
48
  const makerStore = useBrandPageMakerStore();
49
+ const eventTracker = useEventTracker();
42
50
 
43
51
  // The preview must step back for any modal opening over the editor.
44
52
  const isFlowModalOpen = computed(() => containerStore.isAnyModalVisible);
@@ -96,18 +104,34 @@ const announceExit = async (): Promise<void> => {
96
104
  };
97
105
 
98
106
  const onClose = async (): Promise<void> => {
107
+ // Escape funnels through the shell's close emit too, so it counts as an exit.
108
+ eventTracker.sendPreviewModalContinuationEvent(
109
+ PREVIEW_MODAL_CONTINUATION_EVENTS.EXIT,
110
+ props.ctaLocation,
111
+ );
99
112
  emit('close');
100
113
 
101
114
  await announceExit();
102
115
  };
103
116
 
104
117
  const onEdit = async (): Promise<void> => {
118
+ // Mirror the rendered label: cta=publish shows "Back to Editor", else "Edit".
119
+ eventTracker.sendPreviewModalContinuationEvent(
120
+ props.cta === 'publish'
121
+ ? PREVIEW_MODAL_CONTINUATION_EVENTS.BACK_TO_EDITOR
122
+ : PREVIEW_MODAL_CONTINUATION_EVENTS.EDIT,
123
+ props.ctaLocation,
124
+ );
105
125
  emit('edit');
106
126
 
107
127
  await announceExit();
108
128
  };
109
129
 
110
130
  const onPublish = (): void => {
131
+ eventTracker.sendPreviewModalContinuationEvent(
132
+ PREVIEW_MODAL_CONTINUATION_EVENTS.PUBLISH,
133
+ props.ctaLocation,
134
+ );
111
135
  // Route through the host's publish flow so pre-purchase still reaches checkout.
112
136
  publishing.value = true;
113
137
  emit('publish');
@@ -117,6 +141,7 @@ watch(
117
141
  () => props.visible,
118
142
  (isVisible) => {
119
143
  if (isVisible) {
144
+ eventTracker.sendPreviewModalOpenEvent(props.ctaLocation);
120
145
  openPreview();
121
146
  } else {
122
147
  previewUrl.value = '';
@@ -74,6 +74,24 @@ export const EVENTS = {
74
74
  WEBSITE_MAKER_REDO: 'website_maker_redo',
75
75
  WEBSITE_MAKER_HISTORY_CAPPED: 'website_maker_history_capped',
76
76
  WEBSITE_MAKER_HISTORY_OVERSIZED: 'website_maker_history_oversized',
77
+ WEBSITE_MAKER_LIVE_PREVIEW_CLICK: 'website_maker_live_preview_click',
78
+ WEBSITE_MAKER_LIVE_PREVIEW_MODAL: 'website_maker_live_preview_modal',
79
+ };
80
+
81
+ // The global `location` prop already means pre/post purchase, so the surface a
82
+ // preview was launched from rides on cta_location, reusing existing values.
83
+ export const PREVIEW_CTA_LOCATIONS = {
84
+ MAKER: 'maker',
85
+ MANAGE_WEBSITE: 'manage_website',
86
+ };
87
+
88
+ export const PREVIEW_MODAL_CONTINUATION_EVENTS = {
89
+ EXIT: 'exit',
90
+ // The same button reads "Back to Editor" on the editor and "Edit" on other
91
+ // surfaces — the continuation mirrors the label the user actually clicked.
92
+ BACK_TO_EDITOR: 'back_to_editor',
93
+ EDIT: 'edit',
94
+ PUBLISH: 'publish',
77
95
  };
78
96
 
79
97
  export const SELECT_LOCATIONS = {
@@ -426,6 +444,29 @@ const useEventTracker = () => {
426
444
  is_enabled: isEnabled,
427
445
  });
428
446
  },
447
+ // Live preview: the click is tracked separately from the modal opening —
448
+ // sign-in/save gating means a click doesn't always open the modal, and the
449
+ // post-sign-in return trip opens it without a click.
450
+ sendPreviewClickEvent: (ctaLocation: string) => {
451
+ _publishEvent(EVENTS.WEBSITE_MAKER_LIVE_PREVIEW_CLICK, {
452
+ cta_location: ctaLocation,
453
+ });
454
+ },
455
+ sendPreviewModalOpenEvent: (ctaLocation: string) => {
456
+ _publishEvent(EVENTS.WEBSITE_MAKER_LIVE_PREVIEW_MODAL, {
457
+ cta_location: ctaLocation,
458
+ continuation: null,
459
+ });
460
+ },
461
+ sendPreviewModalContinuationEvent: (
462
+ continuation: string,
463
+ ctaLocation: string,
464
+ ) => {
465
+ _publishEvent(EVENTS.WEBSITE_MAKER_LIVE_PREVIEW_MODAL, {
466
+ cta_location: ctaLocation,
467
+ continuation,
468
+ });
469
+ },
429
470
  // Undo / Redo (BP-5506)
430
471
  sendUndoEvent: (props: {
431
472
  entries_count: number;
@@ -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",