@designcrowd/library.brand-page 6.1.3 → 6.1.4-ast-prepurchase

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 (149) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-Lk8J1vHg.js → CreatedOnBrandCrowd-de-de-BFDE6VfS.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-BbXj5fb1.js → CreatedOnBrandCrowd-en-us-CRhl6-Cd.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-CjnqvYCi.js → CreatedOnBrandCrowd-es-es-VaP_-7dv.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Dbjv4qpN.js → CreatedOnBrandCrowd-fr-ca-BySGqvWO.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Dbjv4qpN.js → CreatedOnBrandCrowd-fr-fr-BySGqvWO.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BZIOPEvL.js → CreatedOnBrandCrowd-pt-br-BHjXVcSk.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BZIOPEvL.js → CreatedOnBrandCrowd-pt-pt-BHjXVcSk.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-aN4JMRDI.js → CreatedOnDcom-de-de-DIFO5IZY.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-f3VD6Pz4.js → CreatedOnDcom-en-us-CcpDa04T.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-CQhNdtRP.js → CreatedOnDcom-es-es-DHwY0lWp.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-CFsC_XoO.js → CreatedOnDcom-fr-ca-BP3P0hJL.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-J4ZtCBO9.js → CreatedOnDcom-fr-fr-Bai1giy3.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-C1yFugh_.js → CreatedOnDcom-pt-br-GgKiDrbK.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-ycOo5lDn.js → CreatedOnDcom-pt-pt-uuuxCYR3.js} +1 -1
  15. package/lib/src/PreviewModal-DY2aAN0a.js +19931 -0
  16. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.d.ts +0 -1
  17. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.js +1 -4
  18. package/lib/src/brand-page/models/brand-page-context.model.d.ts +2 -0
  19. package/lib/src/brand-page/models/brand-page-context.model.js +12 -0
  20. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +2 -0
  21. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.js +14 -1
  22. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +134 -0
  23. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +50 -0
  24. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +130 -0
  25. package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.d.ts +1 -0
  26. package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.js +3 -0
  27. package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +14 -0
  28. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +130 -0
  29. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +130 -0
  30. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +130 -0
  31. package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +130 -0
  32. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +130 -0
  33. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +130 -0
  34. package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +1 -0
  35. package/lib/src/brand-page-maker/composables/useEventTracker.js +1 -0
  36. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +132 -0
  37. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +22 -6
  38. package/lib/src/bundled-translations-D42tE0D2.js +179 -0
  39. package/lib/src/bundled-translations.de-DE-D9aWGgFt.js +179 -0
  40. package/lib/src/bundled-translations.es-ES-lk4B0xQ7.js +179 -0
  41. package/lib/src/bundled-translations.fr-CA-DhOBNt6q.js +179 -0
  42. package/lib/src/bundled-translations.fr-FR-CnpafQeG.js +179 -0
  43. package/lib/src/bundled-translations.pt-BR-C4PnKrd9.js +179 -0
  44. package/lib/src/bundled-translations.pt-PT-CfKDVQC-.js +179 -0
  45. package/lib/src/bundles/bundled-translations.de-DE.json +5 -0
  46. package/lib/src/bundles/bundled-translations.es-ES.json +5 -0
  47. package/lib/src/bundles/bundled-translations.fr-CA.json +5 -0
  48. package/lib/src/bundles/bundled-translations.fr-FR.json +5 -0
  49. package/lib/src/bundles/bundled-translations.json +5 -0
  50. package/lib/src/bundles/bundled-translations.pt-BR.json +5 -0
  51. package/lib/src/bundles/bundled-translations.pt-PT.json +5 -0
  52. package/lib/src/clients/brand-page-client.d.ts +1 -0
  53. package/lib/src/composables/useTranslate.d.ts +2 -1
  54. package/lib/src/composables/useTranslate.js +2 -1
  55. package/lib/src/constants/brand-page-maker.d.ts +2 -0
  56. package/lib/src/constants/brand-page-maker.js +5 -0
  57. package/lib/src/constants/live-preview.constants.d.ts +15 -0
  58. package/lib/src/constants/live-preview.constants.js +15 -0
  59. package/lib/src/css/library.brand-page-brandCrowd.min.css +264 -121
  60. package/lib/src/css/library.brand-page-brandPage.min.css +268 -121
  61. package/lib/src/css/library.brand-page-designCom.min.css +264 -121
  62. package/lib/src/css/library.brand-page-preview.min.css +264 -121
  63. package/lib/src/index.d.ts +2 -1
  64. package/lib/src/index.mjs +102150 -225
  65. package/lib/src/lib.js +2 -1
  66. package/lib/src/preview-modal.d.ts +5 -0
  67. package/lib/src/preview-modal.js +12 -0
  68. package/lib/src/preview-modal.mjs +6 -0
  69. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +130 -0
  70. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +130 -0
  71. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +130 -0
  72. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +130 -0
  73. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +144 -0
  74. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +130 -0
  75. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +130 -0
  76. package/lib/src/tracker.mjs +1 -1
  77. package/lib/src/{useEventTracker-DmQ6AkNn.js → useEventTracker-Dmc2jFdN.js} +5 -4
  78. package/package.json +5 -1
  79. package/src/brand-page/components/button/Button.vue +11 -0
  80. package/src/brand-page/components/button/Button.vue.test.ts +90 -0
  81. package/src/brand-page/components/image-gallery/masonry/MasonryGallery.vue +1 -0
  82. package/src/brand-page/components/images-section/ImagesSection.vue +10 -0
  83. package/src/brand-page/components/media/Video.vue +60 -1
  84. package/src/brand-page/components/page-footer/PageFooter.mixin.ts +1 -4
  85. package/src/brand-page/components/page-footer/PageFooter.vue +0 -1
  86. package/src/brand-page/components/payment/Payment.vue +3 -0
  87. package/src/brand-page/components/payment/request-payment/RequestPayment.vue +1 -0
  88. package/src/brand-page/components/payment/support-me/SupportMe.vue +3 -0
  89. package/src/brand-page/components/shop/CartModal.vue +9 -0
  90. package/src/brand-page/components/shop/Shop.vue +19 -0
  91. package/src/brand-page/components/shop/Shop.vue.test.ts +182 -2
  92. package/src/brand-page/components/social-links/SocialLinks.vue +1 -9
  93. package/src/brand-page/components/social-links/SocialLinks.vue.test.ts +28 -8
  94. package/src/brand-page/components/social-links/SocialLinksItems.vue +6 -2
  95. package/src/brand-page/components/testimonial-section/TestimonialSection.vue +2 -2
  96. package/src/brand-page/components/video/Video.vue +3 -0
  97. package/src/brand-page/models/brand-page-context.model.ts +23 -0
  98. package/src/brand-page-maker/components/action-bar/ActionBar.mixin.ts +15 -0
  99. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.test.ts +95 -0
  100. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.vue +19 -4
  101. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +57 -0
  102. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.preview-gate.test.ts +113 -0
  103. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.vue +26 -1
  104. package/src/brand-page-maker/components/navigation/Navigation.mixin.ts +3 -0
  105. package/src/brand-page-maker/components/navigation/Navigation.vue +1 -0
  106. package/src/brand-page-maker/components/preview-modal/PreviewModal.test.ts +162 -0
  107. package/src/brand-page-maker/components/preview-modal/PreviewModal.vue +347 -0
  108. package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.test.ts +226 -0
  109. package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.vue +149 -0
  110. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.de-DE.json +6 -0
  111. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.es-ES.json +6 -0
  112. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.fr-CA.json +6 -0
  113. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.fr-FR.json +6 -0
  114. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.json +6 -0
  115. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.pt-BR.json +6 -0
  116. package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.pt-PT.json +6 -0
  117. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +29 -17
  118. package/src/brand-page-maker/composables/useEventTracker.ts +1 -0
  119. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.save.test.ts +106 -0
  120. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +40 -8
  121. package/src/bundles/bundled-translations.de-DE.json +5 -0
  122. package/src/bundles/bundled-translations.es-ES.json +5 -0
  123. package/src/bundles/bundled-translations.fr-CA.json +5 -0
  124. package/src/bundles/bundled-translations.fr-FR.json +5 -0
  125. package/src/bundles/bundled-translations.json +5 -0
  126. package/src/bundles/bundled-translations.pt-BR.json +5 -0
  127. package/src/bundles/bundled-translations.pt-PT.json +5 -0
  128. package/src/clients/brand-page-client.ts +2 -0
  129. package/src/composables/useTranslate.ts +2 -0
  130. package/src/constants/brand-page-maker.ts +5 -0
  131. package/src/constants/live-preview.constants.ts +15 -0
  132. package/src/lib.ts +5 -1
  133. package/src/preview-modal.ts +14 -0
  134. package/src/shared/common/i18n/common.de-DE.json +1 -0
  135. package/src/shared/common/i18n/common.es-ES.json +1 -0
  136. package/src/shared/common/i18n/common.fr-CA.json +1 -0
  137. package/src/shared/common/i18n/common.fr-FR.json +1 -0
  138. package/src/shared/common/i18n/common.json +1 -0
  139. package/src/shared/common/i18n/common.pt-BR.json +1 -0
  140. package/src/shared/common/i18n/common.pt-PT.json +1 -0
  141. package/src/shared/ecommerce-settings/store/cart-store.test.ts +38 -0
  142. package/lib/src/bundled-translations-2HkRBbBD.js +0 -177
  143. package/lib/src/bundled-translations.de-DE-CL9ZEhU2.js +0 -177
  144. package/lib/src/bundled-translations.es-ES-uj7Sliu0.js +0 -177
  145. package/lib/src/bundled-translations.fr-CA-BMnBtr_w.js +0 -177
  146. package/lib/src/bundled-translations.fr-FR-9PEZedzq.js +0 -177
  147. package/lib/src/bundled-translations.pt-BR-B3oNDkoe.js +0 -177
  148. package/lib/src/bundled-translations.pt-PT-DM7Vsf-h.js +0 -177
  149. package/lib/src/lib-CVwyHyBz.js +0 -121538
@@ -0,0 +1,226 @@
1
+ import { describe, test, expect, beforeEach, vi } from 'vitest';
2
+ import { createPinia, setActivePinia } from 'pinia';
3
+ import { flushPromises, shallowMount } from '@vue/test-utils';
4
+ import PreviewModalWithState from './PreviewModalWithState.vue';
5
+ import PreviewModal from './PreviewModal.vue';
6
+ import {
7
+ useBrandPageContainerStore,
8
+ useUiStore,
9
+ } from '@brand-page-maker/stores';
10
+ import { UI_SCREENSIZE_MODES } from '@shared/models';
11
+
12
+ const PREVIEW_URL = 'https://brand.site/_p/request?token=signed';
13
+
14
+ const mountModal = () =>
15
+ shallowMount(PreviewModalWithState, {
16
+ props: { visible: false },
17
+ });
18
+
19
+ const modalProps = (wrapper: ReturnType<typeof mountModal>) =>
20
+ wrapper.findComponent(PreviewModal).props();
21
+
22
+ describe('PreviewModalWithState', () => {
23
+ let containerStore: ReturnType<typeof useBrandPageContainerStore>;
24
+
25
+ beforeEach(() => {
26
+ setActivePinia(createPinia());
27
+ containerStore = useBrandPageContainerStore();
28
+ containerStore.isProduction = false;
29
+ containerStore.designDraftToken = 'draft-123';
30
+ containerStore.livePreviewUrl = PREVIEW_URL;
31
+ });
32
+
33
+ test('saves first, then points the iframe at the just-saved draft', async () => {
34
+ const save = vi
35
+ .spyOn(containerStore, 'saveBrandPage')
36
+ .mockResolvedValue({ saved: true, canPublish: true } as any);
37
+
38
+ const wrapper = mountModal();
39
+
40
+ await wrapper.setProps({ visible: true });
41
+ await flushPromises();
42
+
43
+ expect(save).toHaveBeenCalledOnce();
44
+
45
+ const props = modalProps(wrapper);
46
+
47
+ expect(props.loading).toBe(false);
48
+ expect(props.errored).toBe(false);
49
+ expect(props.previewUrl).toBe(PREVIEW_URL);
50
+ });
51
+
52
+ test('saves without publishing, so previewing a live site never republishes it', async () => {
53
+ containerStore.brandPageStatus = 'Published';
54
+
55
+ const save = vi
56
+ .spyOn(containerStore, 'saveBrandPage')
57
+ .mockResolvedValue({ saved: true, canPublish: true } as any);
58
+
59
+ const wrapper = mountModal();
60
+
61
+ await wrapper.setProps({ visible: true });
62
+ await flushPromises();
63
+
64
+ expect(containerStore.isPublished).toBe(true);
65
+ expect(save).toHaveBeenCalledWith({ saveOnly: true });
66
+ });
67
+
68
+ test('does not open the preview when the save fails', async () => {
69
+ vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue(false as any);
70
+
71
+ const wrapper = mountModal();
72
+
73
+ await wrapper.setProps({ visible: true });
74
+ await flushPromises();
75
+
76
+ const props = modalProps(wrapper);
77
+
78
+ expect(props.errored).toBe(true);
79
+ expect(props.previewUrl).toBe('');
80
+ });
81
+
82
+ test('errors when the save returns no preview url', async () => {
83
+ containerStore.livePreviewUrl = undefined;
84
+
85
+ vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
86
+ saved: true,
87
+ canPublish: true,
88
+ } as any);
89
+
90
+ const wrapper = mountModal();
91
+
92
+ await wrapper.setProps({ visible: true });
93
+ await flushPromises();
94
+
95
+ const props = modalProps(wrapper);
96
+
97
+ expect(props.errored).toBe(true);
98
+ expect(props.previewUrl).toBe('');
99
+ });
100
+
101
+ test('on a phone the preview fills the screen and the device toggle is gone', async () => {
102
+ useUiStore().screenSizeMode = UI_SCREENSIZE_MODES.mobile;
103
+
104
+ vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
105
+ saved: true,
106
+ canPublish: true,
107
+ } as any);
108
+
109
+ const wrapper = mountModal();
110
+
111
+ await wrapper.setProps({ visible: true });
112
+ await flushPromises();
113
+
114
+ const props = modalProps(wrapper);
115
+
116
+ // Not 'mobile' — that mode draws a phone bezel, which on a phone would be a
117
+ // device frame inside a device.
118
+ expect(props.deviceMode).toBe('desktop');
119
+ expect(props.showDeviceToggle).toBe(false);
120
+ });
121
+
122
+ // The mobile work must not reach desktop — the toggle is still there and it
123
+ // still drives the frame.
124
+ test('desktop keeps the device toggle and honours the viewport it picks', async () => {
125
+ useUiStore().screenSizeMode = UI_SCREENSIZE_MODES.desktop;
126
+
127
+ vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
128
+ saved: true,
129
+ canPublish: true,
130
+ } as any);
131
+
132
+ const wrapper = mountModal();
133
+
134
+ await wrapper.setProps({ visible: true });
135
+ await flushPromises();
136
+
137
+ expect(modalProps(wrapper).showDeviceToggle).toBe(true);
138
+
139
+ await wrapper
140
+ .findComponent(PreviewModal)
141
+ .vm.$emit('update:deviceMode', 'mobile');
142
+ await flushPromises();
143
+
144
+ expect(modalProps(wrapper).deviceMode).toBe('mobile');
145
+ });
146
+
147
+ // The preview sits at z-1000 and drops to z-40 for a host flow modal. Tracking
148
+ // only the publish modal left the "Upgrade to publish" prompt stranded behind the
149
+ // preview, so publishing from inside the preview couldn't be completed.
150
+ test.each([
151
+ ['publish options', 'isPublishModalVisible'],
152
+ ['upgrade prompt', 'isUpgradeModalVisible'],
153
+ ])("steps back so the host's %s modal stacks on top", async (_, flag) => {
154
+ vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
155
+ saved: true,
156
+ canPublish: true,
157
+ } as any);
158
+
159
+ const wrapper = mountModal();
160
+
161
+ await wrapper.setProps({ visible: true });
162
+ await flushPromises();
163
+
164
+ expect(modalProps(wrapper).flowModalOpen).toBe(false);
165
+
166
+ (containerStore as any)[flag] = true;
167
+ await flushPromises();
168
+
169
+ expect(modalProps(wrapper).flowModalOpen).toBe(true);
170
+ });
171
+
172
+ test('publish is delegated to the host publish flow', async () => {
173
+ vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
174
+ saved: true,
175
+ canPublish: true,
176
+ } as any);
177
+
178
+ const wrapper = mountModal();
179
+
180
+ await wrapper.setProps({ visible: true });
181
+ await flushPromises();
182
+
183
+ await wrapper.findComponent(PreviewModal).vm.$emit('publish');
184
+ await flushPromises();
185
+
186
+ // The host (BrandPageMaker → bc-net) runs the actual publish flow so
187
+ // pre-purchase reaches checkout; the wrapper just relays the intent.
188
+ expect(wrapper.emitted('publish')).toHaveLength(1);
189
+ expect(modalProps(wrapper).publishing).toBe(true);
190
+ });
191
+
192
+ // Announced on the way out, not while the preview covers the editor.
193
+ test('raises the deferred save banner when the preview closes', async () => {
194
+ vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
195
+ saved: true,
196
+ canPublish: true,
197
+ } as any);
198
+
199
+ const showSaveSuccessMessage = vi
200
+ .spyOn(containerStore, 'showSaveSuccessMessage')
201
+ .mockResolvedValue(undefined);
202
+
203
+ const wrapper = mountModal();
204
+
205
+ await wrapper.setProps({ visible: true });
206
+ await flushPromises();
207
+
208
+ expect(showSaveSuccessMessage).not.toHaveBeenCalled();
209
+
210
+ await wrapper.findComponent(PreviewModal).vm.$emit('close');
211
+ await flushPromises();
212
+
213
+ expect(wrapper.emitted('close')).toHaveLength(1);
214
+ expect(showSaveSuccessMessage).toHaveBeenCalledOnce();
215
+ });
216
+
217
+ // BP-5957: the modal uppercases its labels on BrandCrowd, not Design.com.
218
+ test.each([true, false])(
219
+ 'passes isDesignCom=%s to the modal',
220
+ (isDesignCom) => {
221
+ containerStore.isDesignCom = isDesignCom;
222
+
223
+ expect(modalProps(mountModal()).isDesignCom).toBe(isDesignCom);
224
+ },
225
+ );
226
+ });
@@ -0,0 +1,149 @@
1
+ <template>
2
+ <PreviewModal
3
+ data-component="preview-modal-with-state"
4
+ :visible="visible"
5
+ :preview-url="previewUrl"
6
+ :edit-label="previewModalTr('edit')"
7
+ :cta="cta"
8
+ :device-mode="effectiveDeviceMode"
9
+ :show-device-toggle="!isMobileScreenSize"
10
+ :is-design-com="containerStore.isDesignCom"
11
+ :loading="loading"
12
+ :errored="errored"
13
+ :publishing="publishing"
14
+ :flow-modal-open="isFlowModalOpen"
15
+ @update:device-mode="deviceMode = $event"
16
+ @edit="onClose"
17
+ @publish="onPublish"
18
+ @retry="openPreview"
19
+ @close="onClose"
20
+ />
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import { ref, computed, watch, type PropType } from 'vue';
25
+ import PreviewModal from './PreviewModal.vue';
26
+ import {
27
+ useBrandPageContainerStore,
28
+ useBrandPageMakerStore,
29
+ useUiStore,
30
+ } from '@brand-page-maker/stores';
31
+ import { BRAND_PAGE_TYPES } from '@brand-page';
32
+ import { previewModalTr } from '@src/composables/useTranslate';
33
+ import { UI_SCREENSIZE_MODES, type UiDeviceMode } from '@shared/models';
34
+
35
+ const props = defineProps({
36
+ visible: { type: Boolean, default: false },
37
+ cta: {
38
+ type: String as PropType<'publish' | 'edit' | 'none'>,
39
+ default: 'publish',
40
+ },
41
+ });
42
+
43
+ const emit = defineEmits<{ close: []; publish: [] }>();
44
+
45
+ const containerStore = useBrandPageContainerStore();
46
+ const makerStore = useBrandPageMakerStore();
47
+ const uiStore = useUiStore();
48
+
49
+ const isMobileScreenSize = computed(
50
+ () => uiStore.screenSizeMode === UI_SCREENSIZE_MODES.mobile,
51
+ );
52
+
53
+ // The preview must step back for any modal opening over the editor.
54
+ const isFlowModalOpen = computed(() => containerStore.isAnyModalVisible);
55
+
56
+ // brandSite is desktop-primary; brand-page and brand-contact are mobile-first.
57
+ const defaultDeviceMode = (): UiDeviceMode =>
58
+ makerStore.brandPageType === BRAND_PAGE_TYPES.brandSite
59
+ ? 'desktop'
60
+ : 'mobile';
61
+
62
+ const deviceMode = ref<UiDeviceMode>(defaultDeviceMode());
63
+
64
+ const effectiveDeviceMode = computed<UiDeviceMode>(() =>
65
+ isMobileScreenSize.value ? 'desktop' : deviceMode.value,
66
+ );
67
+
68
+ const previewUrl = ref('');
69
+ const loading = ref(false);
70
+ const errored = ref(false);
71
+ const publishing = ref(false);
72
+
73
+ // Save first so the preview is never stale; saveOnly stops a live site republishing.
74
+ const openPreview = async (): Promise<void> => {
75
+ loading.value = true;
76
+ errored.value = false;
77
+ previewUrl.value = '';
78
+
79
+ // Pre-purchase the host owns the draft and already saved it.
80
+ if (!containerStore.isPrePurchase) {
81
+ const result = await containerStore.saveBrandPage({ saveOnly: true });
82
+
83
+ if (!result || !result.saved) {
84
+ errored.value = true;
85
+ loading.value = false;
86
+
87
+ return;
88
+ }
89
+ }
90
+
91
+ // The save hands back the signed preview URL — no second call needed.
92
+ const url = containerStore.livePreviewUrl;
93
+
94
+ if (!url) {
95
+ errored.value = true;
96
+ loading.value = false;
97
+
98
+ return;
99
+ }
100
+
101
+ previewUrl.value = url;
102
+ loading.value = false;
103
+ };
104
+
105
+ // The preview covers the editor, so its save is announced on the way out. No-ops
106
+ // when the save wasn't ours (pre-purchase) or was already announced (publish).
107
+ const onClose = async (): Promise<void> => {
108
+ emit('close');
109
+
110
+ await containerStore.showSaveSuccessMessage();
111
+ await makerStore.showNextNotificationMessage();
112
+ };
113
+
114
+ const onPublish = (): void => {
115
+ // Route through the host's publish flow so pre-purchase still reaches checkout.
116
+ publishing.value = true;
117
+ emit('publish');
118
+ };
119
+
120
+ watch(
121
+ () => props.visible,
122
+ (isVisible) => {
123
+ if (isVisible) {
124
+ deviceMode.value = defaultDeviceMode();
125
+ openPreview();
126
+ } else {
127
+ previewUrl.value = '';
128
+ errored.value = false;
129
+ publishing.value = false;
130
+ }
131
+ },
132
+ );
133
+
134
+ // Clear the publishing overlay once the host's publish flow has taken over.
135
+ watch(
136
+ () => containerStore.isSaving,
137
+ (saving, wasSaving) => {
138
+ if (publishing.value && wasSaving && !saving) {
139
+ publishing.value = false;
140
+ }
141
+ },
142
+ );
143
+
144
+ watch(isFlowModalOpen, (isOpen) => {
145
+ if (publishing.value && isOpen) {
146
+ publishing.value = false;
147
+ }
148
+ });
149
+ </script>
@@ -0,0 +1,6 @@
1
+ {
2
+ "previewModal": {
3
+ "edit": "Bearbeiten",
4
+ "retry": "Erneut versuchen"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "previewModal": {
3
+ "edit": "Editar",
4
+ "retry": "Vuelve a intentarlo"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "previewModal": {
3
+ "edit": "Éditer",
4
+ "retry": "Réessayez"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "previewModal": {
3
+ "edit": "Éditer",
4
+ "retry": "Réessayez"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "previewModal": {
3
+ "edit": "Edit",
4
+ "retry": "Retry"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "previewModal": {
3
+ "edit": "Editar",
4
+ "retry": "Tente novamente"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "previewModal": {
3
+ "edit": "Editar",
4
+ "retry": "Repetir"
5
+ }
6
+ }
@@ -63,7 +63,7 @@
63
63
  <div class="style-editor">
64
64
  <Dropdown
65
65
  ref="textStyleTypeDropdownRef"
66
- v-click-outside="(e: MouseEvent) => clickOutsideTextStyle(e)"
66
+ v-click-outside="(e: Event) => clickOutsideTextStyle(e)"
67
67
  data-test-tag="text-style-dropdown"
68
68
  class="style-editor-button style-editor-button-textstyle"
69
69
  :title="
@@ -96,7 +96,7 @@
96
96
  </Dropdown>
97
97
  <Dropdown
98
98
  ref="fontStyleDropdownRef"
99
- v-click-outside="(e: MouseEvent) => clickOutsideFontStyle(e)"
99
+ v-click-outside="(e: Event) => clickOutsideFontStyle(e)"
100
100
  data-test-tag="font-style-dropdown"
101
101
  :title="fontSettings.selectedFont"
102
102
  icon-size="xs"
@@ -305,7 +305,7 @@
305
305
  <div
306
306
  v-if="isSpacingDropdownOpen"
307
307
  ref="spacingPopupRef"
308
- v-click-outside="(e: MouseEvent) => clickOutsideTextSpacing(e)"
308
+ v-click-outside="(e: Event) => clickOutsideTextSpacing(e)"
309
309
  class="popup-container tw-max-w-[310px] spacing-popup"
310
310
  data-test-tag="spacing-toolbar"
311
311
  >
@@ -335,7 +335,7 @@
335
335
  <div
336
336
  v-if="isHyperlinkPopupOpen"
337
337
  ref="linkPopupRef"
338
- v-click-outside="(e: MouseEvent) => clickOutsideHyperlink(e)"
338
+ v-click-outside="(e: Event) => clickOutsideHyperlink(e)"
339
339
  class="popup-container tw-w-[300px] hyperlink-popup"
340
340
  data-test-tag="hyperlink-toolbar"
341
341
  >
@@ -886,31 +886,43 @@ const onSelectTextStyleType = (option: ButtonGroupOption) => {
886
886
  textStyleTypeDropdownRef.value?.hideMenu();
887
887
  };
888
888
 
889
- // Event handlers for dropdowns and popups
890
- const clickOutsideTextStyle = (event: MouseEvent) => {
891
- const target = event.target as HTMLElement;
892
- if (!target.closest('.toolbar-editor-button')) {
889
+ // Event handlers for dropdowns and popups.
890
+ //
891
+ // `v-click-outside` also fires these for a WINDOW BLUR, not just a click: its
892
+ // `detectIframe` option (on by default) listens for blur to catch "focus moved into
893
+ // an iframe", and hands the blur event straight to this handler. A blur event's
894
+ // target is `window`, which has no `closest` — so reading it as an element threw
895
+ // `target.closest is not a function`. Latent until the editor gained an iframe (the
896
+ // live preview), which is exactly the case `detectIframe` exists to report.
897
+ //
898
+ // Focus leaving for an iframe IS outside the toolbar, so a non-element target must
899
+ // close the menu, not bail out.
900
+ const isOutside = (event: Event, selector: string): boolean => {
901
+ const { target } = event;
902
+
903
+ return !(target instanceof Element) || !target.closest(selector);
904
+ };
905
+
906
+ const clickOutsideTextStyle = (event: Event) => {
907
+ if (isOutside(event, '.toolbar-editor-button')) {
893
908
  textStyleTypeDropdownRef.value?.hideMenu();
894
909
  }
895
910
  };
896
911
 
897
- const clickOutsideFontStyle = (event: MouseEvent) => {
898
- const target = event.target as HTMLElement;
899
- if (!target.closest('.toolbar-editor-button')) {
912
+ const clickOutsideFontStyle = (event: Event) => {
913
+ if (isOutside(event, '.toolbar-editor-button')) {
900
914
  fontStyleDropdownRef.value?.hideMenu();
901
915
  }
902
916
  };
903
917
 
904
- const clickOutsideTextSpacing = (event: MouseEvent) => {
905
- const target = event.target as HTMLElement;
906
- if (!target.closest('.spacing-popup')) {
918
+ const clickOutsideTextSpacing = (event: Event) => {
919
+ if (isOutside(event, '.spacing-popup')) {
907
920
  isSpacingDropdownOpen.value = false;
908
921
  }
909
922
  };
910
923
 
911
- const clickOutsideHyperlink = (event: MouseEvent) => {
912
- const target = event.target as HTMLElement;
913
- if (!target.closest('.hyperlink-popup')) {
924
+ const clickOutsideHyperlink = (event: Event) => {
925
+ if (isOutside(event, '.hyperlink-popup')) {
914
926
  isHyperlinkPopupOpen.value = false;
915
927
  }
916
928
  };
@@ -92,6 +92,7 @@ export const UPGRADE_CTA_LOCATIONS = {
92
92
  ADD_CONTENT: 'add_content',
93
93
  EDITOR_PUBLISH_TOP: 'editor_publish_top',
94
94
  EDITOR_PUBLISH_BOTTOM: 'editor_publish_bottom',
95
+ EDITOR_PREVIEW_PUBLISH: 'editor_preview_publish',
95
96
  SLUG_CHANGE_MAKER_SIDEBAR_SETTINGS: 'slug_change_maker_sidebar_settings',
96
97
  SLUG_CHANGE_MANAGE_PAGE: 'slug_change_manage_page',
97
98
  GOOGLE_ADSENSE: 'google_adsense',
@@ -0,0 +1,106 @@
1
+ import { describe, test, expect, beforeEach, vi } from 'vitest';
2
+ import { createPinia, setActivePinia } from 'pinia';
3
+ import { useBrandPageContainerStore } from '@brand-page-maker/stores';
4
+ import brandPageClient from '../../../clients/brand-page-client';
5
+
6
+ vi.mock('../../../clients/brand-page-client', () => ({
7
+ default: {
8
+ saveBrandPageAsync: vi.fn(),
9
+ },
10
+ }));
11
+
12
+ const SAVED = {
13
+ designDraftToken: 'draft-123',
14
+ status: 'Published',
15
+ slug: 'my-site',
16
+ canPublish: true,
17
+ };
18
+
19
+ describe('brand-page-container-store saveBrandPage', () => {
20
+ let store: ReturnType<typeof useBrandPageContainerStore>;
21
+
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ setActivePinia(createPinia());
25
+
26
+ store = useBrandPageContainerStore();
27
+ store.designDraftToken = 'draft-123';
28
+ store.brandPageStatus = 'Published';
29
+
30
+ // The export is exercised by its own tests; stub it so these cover only the
31
+ // endpoint choice.
32
+ vi.spyOn(store, 'exportDesignAsync').mockResolvedValue({
33
+ shouldPublish: true,
34
+ } as any);
35
+
36
+ vi.mocked(brandPageClient.saveBrandPageAsync).mockResolvedValue(
37
+ SAVED as any,
38
+ );
39
+ });
40
+
41
+ const requestBody = () =>
42
+ vi.mocked(brandPageClient.saveBrandPageAsync).mock.calls[0][1];
43
+
44
+ test('saveOnly asks a published site not to republish', async () => {
45
+ await store.saveBrandPage({ saveOnly: true });
46
+
47
+ expect(brandPageClient.saveBrandPageAsync).toHaveBeenCalledOnce();
48
+ expect(requestBody()).toMatchObject({ saveOnly: true });
49
+ });
50
+
51
+ test('a normal save leaves saveOnly off', async () => {
52
+ await store.saveBrandPage();
53
+
54
+ expect(requestBody()).toMatchObject({ saveOnly: false });
55
+ });
56
+
57
+ // The preview covers the editor, so the banner would land behind it. The trigger
58
+ // stays UP so the preview can raise it on close.
59
+ test('saveOnly defers the save-success banner instead of raising it', async () => {
60
+ const showSaveSuccessMessage = vi
61
+ .spyOn(store, 'showSaveSuccessMessage')
62
+ .mockResolvedValue(undefined);
63
+
64
+ await store.saveBrandPage({ saveOnly: true });
65
+
66
+ expect(showSaveSuccessMessage).not.toHaveBeenCalled();
67
+ expect(store.triggerSaveSuccessMessage).toBe(true);
68
+ });
69
+
70
+ test('a normal save still raises the banner', async () => {
71
+ const showSaveSuccessMessage = vi
72
+ .spyOn(store, 'showSaveSuccessMessage')
73
+ .mockResolvedValue(undefined);
74
+
75
+ await store.saveBrandPage();
76
+
77
+ expect(showSaveSuccessMessage).toHaveBeenCalled();
78
+ expect(store.triggerSaveSuccessMessage).toBe(true);
79
+ });
80
+
81
+ // The live preview is a full-viewport takeover that steps back on this getter, so a
82
+ // modal missing from it opens BEHIND the preview — the bug this replaced, three times.
83
+ test.each([
84
+ ['isPublishModalVisible'],
85
+ ['isUpgradeModalVisible'],
86
+ ['isContextualUpgradeModalVisible'],
87
+ ['isStripeOnboardingModalVisible'],
88
+ ['isHostModalVisible'],
89
+ ])('isAnyModalVisible covers %s', (flag) => {
90
+ expect(store.isAnyModalVisible).toBe(false);
91
+
92
+ store[flag] = true;
93
+
94
+ expect(store.isAnyModalVisible).toBe(true);
95
+ });
96
+
97
+ test('saveOnly does not change shouldPublish', async () => {
98
+ await store.saveBrandPage({ saveOnly: true });
99
+
100
+ // `shouldPublish` stays derived from isPublished. Sending false would flip
101
+ // BrandPageService's `!shouldPublish` branch and start firing a
102
+ // "brand page saved" marketing event on every preview open.
103
+ expect(store.exportDesignAsync).toHaveBeenCalledWith();
104
+ expect(requestBody()).toMatchObject({ shouldPublish: true });
105
+ });
106
+ });