@designcrowd/library.brand-page 6.0.13 → 6.0.14-5804-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-CBjFFeBP.js → CreatedOnBrandCrowd-de-de-BYABvnQX.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-NuNXF81K.js → CreatedOnBrandCrowd-en-us-CU6aE_W2.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BJG8nfug.js → CreatedOnBrandCrowd-es-es-Pfuc4Nfl.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-uX-nL53e.js → CreatedOnBrandCrowd-fr-ca-DyQ1RyZN.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-uX-nL53e.js → CreatedOnBrandCrowd-fr-fr-DyQ1RyZN.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-DTrgFGQV.js → CreatedOnBrandCrowd-pt-br-DXMSa1wa.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-DTrgFGQV.js → CreatedOnBrandCrowd-pt-pt-DXMSa1wa.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-DSfpxioT.js → CreatedOnDcom-de-de-kGnTiAMq.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-C7G4gKem.js → CreatedOnDcom-en-us-CKrgHXUY.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-DVckCaji.js → CreatedOnDcom-es-es-CVxOQoOy.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-C5GDrm_C.js → CreatedOnDcom-fr-ca-BduB1A0z.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-v5eMm-j2.js → CreatedOnDcom-fr-fr-LGhd51py.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-Dq3-fPUc.js → CreatedOnDcom-pt-br-BLqHx-RQ.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-DFsncYax.js → CreatedOnDcom-pt-pt-52goGQ1s.js} +1 -1
  15. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -0
  16. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +5 -0
  17. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +1 -1
  18. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.js +4 -6
  19. package/lib/src/brand-page-maker/composables/useQuickLinkBarVisibility.d.ts +17 -0
  20. package/lib/src/brand-page-maker/composables/useQuickLinkBarVisibility.js +23 -0
  21. package/lib/src/css/library.brand-page-brandCrowd.min.css +29 -32
  22. package/lib/src/css/library.brand-page-brandPage.min.css +29 -32
  23. package/lib/src/css/library.brand-page-designCom.min.css +29 -32
  24. package/lib/src/css/library.brand-page-preview.min.css +29 -32
  25. package/lib/src/index.mjs +1 -1
  26. package/lib/src/{lib-BG-pyXi9.js → lib-ClM5iHnZ.js} +11321 -11310
  27. package/package.json +1 -1
  28. package/src/brand-page/components/button/Button.vue +20 -0
  29. package/src/brand-page/components/button/Button.vue.test.ts +175 -0
  30. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +5 -0
  31. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.vue +2 -2
  32. package/src/brand-page-maker/components/device/Device.vue +0 -1
  33. package/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.ts +4 -7
  34. package/src/brand-page-maker/composables/useQuickLinkBarVisibility.test.ts +81 -0
  35. package/src/brand-page-maker/composables/useQuickLinkBarVisibility.ts +25 -0
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.0.13",
4
+ "version": "6.0.14-5804-1",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -12,6 +12,7 @@
12
12
  class="tw-no-underline"
13
13
  :to="{ name: routes.name, params: routes.routeParams }"
14
14
  :class="{ 'tw-w-full': isBrushStrokeIcon }"
15
+ @click="onPageLinkClick"
15
16
  >
16
17
  <div
17
18
  :title="viewModel.title"
@@ -101,6 +102,7 @@
101
102
 
102
103
  <script setup lang="ts">
103
104
  import { computed, ref } from 'vue';
105
+ import { useRouter } from 'vue-router';
104
106
  import { HashRouteModal } from '@designcrowd/fe-shared-lib';
105
107
  import { isEmail, isTel, isURL } from '../../../utils/validator.util';
106
108
  import type { BrandPageContext } from '@brand-page/models';
@@ -231,6 +233,24 @@ const buttonStateClass = computed(() => ({
231
233
  'tw-w-full': isBrushStrokeIcon.value,
232
234
  }));
233
235
 
236
+ const router = useRouter();
237
+
238
+ const onPageLinkClick = (): void => {
239
+ // A page-link pointing to its own page (e.g. a "back to top" button) resolves
240
+ // to the current route. Vue Router dedupes that as a redundant navigation and
241
+ // never runs the global guard that scrolls to top, so trigger it explicitly.
242
+ if (!router) return;
243
+
244
+ const target = router.resolve({
245
+ name: routes.value.name,
246
+ params: routes.value.routeParams,
247
+ });
248
+
249
+ if (target.fullPath === router.currentRoute.value.fullPath) {
250
+ window.scrollTo(0, 0);
251
+ }
252
+ };
253
+
234
254
  const onClick = (): void => {
235
255
  if (isModalUrl.value && isModalImageUrlValid.value) {
236
256
  isModalVisible.value = true;
@@ -0,0 +1,175 @@
1
+ import { vi } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import { createRouter, createMemoryHistory, RouterLink } from 'vue-router';
4
+ import { createPinia, setActivePinia } from 'pinia';
5
+ import Button from './Button.vue';
6
+ import {
7
+ BRAND_PAGE_TYPES,
8
+ MEDIA_SIZES,
9
+ type BrandPageContext,
10
+ } from '@brand-page/models';
11
+ import type {
12
+ ButtonViewModel,
13
+ NavItemViewModel,
14
+ } from '@brand-page/view-models';
15
+
16
+ global.ResizeObserver = vi.fn().mockImplementation(() => ({
17
+ observe: vi.fn(),
18
+ unobserve: vi.fn(),
19
+ disconnect: vi.fn(),
20
+ }));
21
+
22
+ const LINKED_PAGE_ID = 'page-2';
23
+ const LINKED_PAGE_PATH = '/my-brand/second-page';
24
+ const OTHER_PAGE_PATH = '/my-brand';
25
+
26
+ const createTestNavItem = (): NavItemViewModel => ({
27
+ pageId: LINKED_PAGE_ID,
28
+ slug: 'my-brand',
29
+ displayName: 'Second Page',
30
+ routeName: 'page-route',
31
+ routeParams: { slug: 'my-brand', pageSlug: 'second-page' },
32
+ isRoot: false,
33
+ isHidden: false,
34
+ relativePath: 'second-page',
35
+ nestedNavItems: [],
36
+ });
37
+
38
+ const createTestBrandPageContext = (): BrandPageContext => ({
39
+ brandPageSlug: 'my-brand',
40
+ brandPageToken: '',
41
+ isMobile: false,
42
+ isPreview: true,
43
+ isProduction: false,
44
+ isServerRender: false,
45
+ options: {
46
+ mediaSizes: MEDIA_SIZES.sizeThumb,
47
+ },
48
+ isTemplate: false,
49
+ isDesignCom: false,
50
+ pageTitle: 'My Brand',
51
+ runtimeEnvironment: '',
52
+ stripePublishableKey: undefined,
53
+ customDomain: undefined,
54
+ brandPageType: BRAND_PAGE_TYPES.brandSite,
55
+ isBrandPage: false,
56
+ isBrandPageOrIsBrandContact: false,
57
+ isBrandSite: true,
58
+ isBrandContact: false,
59
+ pages: [],
60
+ isCapturingPreview: false,
61
+ recaptchaSiteKey: undefined,
62
+ nav: {
63
+ navItems: [createTestNavItem()],
64
+ },
65
+ });
66
+
67
+ const textStyle = {
68
+ fontSize: '1rem',
69
+ lineHeight: '1.5rem',
70
+ fontFamily: 'Lato',
71
+ textAlign: 'center',
72
+ overflowWrap: 'anywhere',
73
+ wordBreak: 'normal',
74
+ };
75
+
76
+ const createTestButtonViewModel = (
77
+ overrides: Partial<ButtonViewModel> = {},
78
+ ): ButtonViewModel =>
79
+ ({
80
+ id: 'button-1',
81
+ type: 'button',
82
+ role: 'banner-section',
83
+ title: 'Go to top',
84
+ url: '',
85
+ page: LINKED_PAGE_ID,
86
+ pageSlug: '',
87
+ buttonLinkType: 'page',
88
+ clickActionField: 'url',
89
+ iconUrl: '',
90
+ modalImageUrl: '',
91
+ titleStyle: textStyle,
92
+ styles: { selectedButtonStyleId: 'BUTTON_STYLE_1', style: {} },
93
+ onClicked: vi.fn(),
94
+ ...overrides,
95
+ }) as ButtonViewModel;
96
+
97
+ const createTestRouter = () =>
98
+ createRouter({
99
+ history: createMemoryHistory(),
100
+ routes: [
101
+ {
102
+ path: '/:slug/:pageSlug',
103
+ name: 'page-route',
104
+ component: { template: '<div />' },
105
+ },
106
+ {
107
+ path: '/:slug',
108
+ name: 'home-route',
109
+ component: { template: '<div />' },
110
+ },
111
+ ],
112
+ });
113
+
114
+ const mountButton = async (currentPath: string) => {
115
+ const router = createTestRouter();
116
+
117
+ router.push(currentPath);
118
+ await router.isReady();
119
+
120
+ const wrapper = mount(Button, {
121
+ propsData: {
122
+ context: createTestBrandPageContext(),
123
+ viewModel: createTestButtonViewModel(),
124
+ },
125
+ global: {
126
+ plugins: [router],
127
+ },
128
+ });
129
+
130
+ return { wrapper, router };
131
+ };
132
+
133
+ describe('Button - page link "back to top" behaviour (BP-5834)', () => {
134
+ let scrollToSpy: ReturnType<typeof vi.spyOn>;
135
+
136
+ beforeEach(() => {
137
+ setActivePinia(createPinia());
138
+ scrollToSpy = vi
139
+ .spyOn(window, 'scrollTo')
140
+ .mockImplementation(() => undefined);
141
+ });
142
+
143
+ afterEach(() => {
144
+ scrollToSpy.mockRestore();
145
+ });
146
+
147
+ test('scrolls to top when the page-link target is the current route (duplicated navigation is skipped by the router)', async () => {
148
+ // A banner "back to top" button points to the page it lives on. Vue Router
149
+ // dedupes the navigation to the current location and never runs the guard
150
+ // that scrolls to top, so the button must scroll explicitly.
151
+ const { wrapper } = await mountButton(LINKED_PAGE_PATH);
152
+
153
+ const link = wrapper.findComponent(RouterLink);
154
+
155
+ expect(link.exists()).toBe(true);
156
+
157
+ await link.get('a').trigger('click');
158
+
159
+ expect(scrollToSpy).toHaveBeenCalledWith(0, 0);
160
+ });
161
+
162
+ test('does not scroll to top when the page-link target is a different page', async () => {
163
+ // A page-link to another page is a real navigation; the router guard owns
164
+ // scroll-to-top there, so the button must not scroll on its own.
165
+ const { wrapper } = await mountButton(OTHER_PAGE_PATH);
166
+
167
+ const link = wrapper.findComponent(RouterLink);
168
+
169
+ expect(link.exists()).toBe(true);
170
+
171
+ await link.get('a').trigger('click');
172
+
173
+ expect(scrollToSpy).not.toHaveBeenCalled();
174
+ });
175
+ });
@@ -37,6 +37,7 @@ import {
37
37
  } from '@brand-page-maker/stores';
38
38
  import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
39
39
  import { useUndoRedoFlag } from '@brand-page-maker/composables/useUndoRedoFlag';
40
+ import { useQuickLinkBarVisibility } from '@brand-page-maker/composables/useQuickLinkBarVisibility';
40
41
  import type { AdminPreviewLogoTemplateModel } from '@brand-page-maker/stores/admin-logo-settings';
41
42
  import { useUiContentSettingsStore } from '@shared/content-settings/store';
42
43
  import { useUiTemplateSettingsStore } from '@shared/template-settings/store';
@@ -137,6 +138,10 @@ export default defineComponent({
137
138
  isSaving: computed(() => brandPageMakerStore.isSaving),
138
139
  isPublishing: computed(() => brandPageMakerStore.isPublishing),
139
140
  isPublished: computed(() => brandPageMakerStore.isPublished),
141
+ // Whether the QuickLinkBar renders — used to offset the preview (Device)
142
+ // and SideBar clear of the fixed bar on mobile. Must match the bar's own
143
+ // render condition, so it shares the same predicate.
144
+ shouldShowQuickLinkBar: useQuickLinkBarVisibility(),
140
145
  isBrandPageOrIsBrandContact: computed(
141
146
  () => brandPageMakerStore.isBrandPageOrIsBrandContact,
142
147
  ),
@@ -90,7 +90,7 @@
90
90
  :is-mobile-device="isMobileDevice"
91
91
  :is-design-com="isDesignCom"
92
92
  :container="previewContainer"
93
- :has-quick-links="isPublished"
93
+ :has-quick-links="shouldShowQuickLinkBar"
94
94
  >
95
95
  <div ref="previewContent" class="tw-h-full">
96
96
  <PreviewWithState :device-mode="deviceMode" />
@@ -127,7 +127,7 @@
127
127
  <SideBar
128
128
  :mobile-tab-name="SideBarTabNames.PREVIEW"
129
129
  :is-mobile-screen-size="isMobileScreenSize"
130
- :has-quick-links="isPublished"
130
+ :has-quick-links="shouldShowQuickLinkBar"
131
131
  >
132
132
  <template #tab>
133
133
  <component
@@ -11,7 +11,6 @@
11
11
  'tw-opacity-0': !visible,
12
12
  'tw-pt-8': !hasQuickLinks && isMobileDevice,
13
13
  'tw-pt-20 lg:tw-pt-8': hasQuickLinks && isMobileDevice,
14
- 'tw-pt-10': hasQuickLinks && isMobileDevice,
15
14
  }"
16
15
  tabindex="0"
17
16
  >
@@ -5,6 +5,7 @@ import {
5
5
  useEventManagerStore,
6
6
  } from '@brand-page-maker/stores';
7
7
  import { useUndoRedoFlag } from '@brand-page-maker/composables/useUndoRedoFlag';
8
+ import { useQuickLinkBarVisibility } from '@brand-page-maker/composables/useQuickLinkBarVisibility';
8
9
  import { quickLinkBarTr } from '@src/composables/useTranslate';
9
10
 
10
11
  export default defineComponent({
@@ -23,6 +24,9 @@ export default defineComponent({
23
24
  brandPageContainerStore: useBrandPageContainerStore(),
24
25
  eventManagerStore: useEventManagerStore(),
25
26
  undoRedoEnabled: useUndoRedoFlag(),
27
+ // Single source of truth for whether the bar renders — shared with the
28
+ // layout components (Device, SideBar) that offset content clear of it.
29
+ shouldShowBar: useQuickLinkBarVisibility(),
26
30
  };
27
31
  },
28
32
  data() {
@@ -35,13 +39,6 @@ export default defineComponent({
35
39
  return this.brandPageContainerStore.isPublished;
36
40
  },
37
41
 
38
- // Only render the bar when at least one inner cluster has something to
39
- // show. Otherwise we'd render an empty white strip — e.g. on an
40
- // unpublished page with the undo flag off.
41
- shouldShowBar(): boolean {
42
- return Boolean(this.undoRedoEnabled) || this.isPublished;
43
- },
44
-
45
42
  brandPageUrl(): string {
46
43
  if (
47
44
  this.brandPageContainerStore.isCustomDomainEnabled &&
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { useQuickLinkBarVisibility } from './useQuickLinkBarVisibility';
3
+
4
+ // The QuickLinkBar renders when it has something to show — the undo/redo
5
+ // controls (flag on, non-admin) OR the published-URL cluster. This predicate
6
+ // is the single source of truth shared by the bar and the layout components
7
+ // (Device, SideBar) that offset content clear of the fixed bar on mobile.
8
+ //
9
+ // The regression it guards: an unpublished page with the undo/redo flag on
10
+ // still shows the bar, so the offset must treat it as visible — otherwise the
11
+ // fixed bar overlaps the mobile preview.
12
+ //
13
+ // The real useUndoRedoFlag is exercised (only its underlying stores are
14
+ // mocked) so admin gating is covered end-to-end.
15
+
16
+ const containerStoreMock = { isPublished: false };
17
+
18
+ vi.mock('@brand-page-maker/stores', () => ({
19
+ useBrandPageContainerStore: vi.fn(() => containerStoreMock),
20
+ }));
21
+
22
+ const userStoreMock = {
23
+ features: { canUseUndoRedo: false },
24
+ isAdmin: false,
25
+ };
26
+
27
+ vi.mock('@brand-page-maker/stores/user', () => ({
28
+ useBrandPageMakerUserStore: vi.fn(() => userStoreMock),
29
+ }));
30
+
31
+ beforeEach(() => {
32
+ containerStoreMock.isPublished = false;
33
+ userStoreMock.features.canUseUndoRedo = false;
34
+ userStoreMock.isAdmin = false;
35
+ });
36
+
37
+ describe('useQuickLinkBarVisibility', () => {
38
+ it('is visible when unpublished but the undo/redo flag is on (the overlap regression)', () => {
39
+ containerStoreMock.isPublished = false;
40
+ userStoreMock.features.canUseUndoRedo = true;
41
+
42
+ expect(useQuickLinkBarVisibility().value).toBe(true);
43
+ });
44
+
45
+ it('is hidden when unpublished and the undo/redo flag is off', () => {
46
+ containerStoreMock.isPublished = false;
47
+ userStoreMock.features.canUseUndoRedo = false;
48
+
49
+ expect(useQuickLinkBarVisibility().value).toBe(false);
50
+ });
51
+
52
+ it('is visible when published even with the undo/redo flag off', () => {
53
+ containerStoreMock.isPublished = true;
54
+ userStoreMock.features.canUseUndoRedo = false;
55
+
56
+ expect(useQuickLinkBarVisibility().value).toBe(true);
57
+ });
58
+
59
+ it('is visible when both published and the undo/redo flag is on', () => {
60
+ containerStoreMock.isPublished = true;
61
+ userStoreMock.features.canUseUndoRedo = true;
62
+
63
+ expect(useQuickLinkBarVisibility().value).toBe(true);
64
+ });
65
+
66
+ it('gates the undo/redo reason for admins: unpublished admin stays hidden despite the flag', () => {
67
+ containerStoreMock.isPublished = false;
68
+ userStoreMock.features.canUseUndoRedo = true;
69
+ userStoreMock.isAdmin = true;
70
+
71
+ expect(useQuickLinkBarVisibility().value).toBe(false);
72
+ });
73
+
74
+ it('stays visible for a published admin (published cluster is not admin-gated)', () => {
75
+ containerStoreMock.isPublished = true;
76
+ userStoreMock.features.canUseUndoRedo = true;
77
+ userStoreMock.isAdmin = true;
78
+
79
+ expect(useQuickLinkBarVisibility().value).toBe(true);
80
+ });
81
+ });
@@ -0,0 +1,25 @@
1
+ import { computed, type ComputedRef } from 'vue';
2
+ import { useBrandPageContainerStore } from '@brand-page-maker/stores';
3
+ import { useUndoRedoFlag } from './useUndoRedoFlag';
4
+
5
+ /**
6
+ * Whether the QuickLinkBar is rendered in the maker.
7
+ *
8
+ * The bar shows when at least one of its inner clusters has something to
9
+ * display: the undo/redo controls (flag on, non-admin — see
10
+ * {@link useUndoRedoFlag}) OR the published-URL cluster (page published).
11
+ *
12
+ * This is the single source of truth shared by the bar itself
13
+ * (`QuickLinkBar`) and the layout components that offset their content to sit
14
+ * clear of the fixed bar on mobile (`Device`, `SideBar`). Keeping the offset
15
+ * bound to the same predicate the bar renders on prevents the two from
16
+ * drifting — a drift where the bar showed for the undo/redo reason but the
17
+ * offset only accounted for the published reason caused the bar to overlap
18
+ * the mobile preview.
19
+ */
20
+ export function useQuickLinkBarVisibility(): ComputedRef<boolean> {
21
+ const containerStore = useBrandPageContainerStore();
22
+ const undoRedoEnabled = useUndoRedoFlag();
23
+
24
+ return computed(() => undoRedoEnabled.value || containerStore.isPublished);
25
+ }