@shopware/cms-base-layer 1.5.1 → 2.1.0
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.
- package/README.md +398 -12
- package/app/app.config.ts +18 -0
- package/app/assets/icons/check-circle.svg +3 -0
- package/app/assets/icons/checkmark.svg +3 -0
- package/app/assets/icons/chevron.svg +3 -0
- package/app/assets/icons/exclamation-circle.svg +3 -0
- package/app/assets/icons/star-empty.svg +3 -0
- package/app/assets/icons/star-filled.svg +3 -0
- package/app/assets/icons/user.svg +1 -0
- package/app/components/SwCategoryNavigation.vue +83 -0
- package/app/components/SwCategoryNavigationLink.vue +128 -0
- package/{components → app/components}/SwContactForm.vue +27 -27
- package/app/components/SwFilterChips.vue +144 -0
- package/app/components/SwFilterDropdown.vue +54 -0
- package/app/components/SwListingProductPrice.vue +89 -0
- package/{components → app/components}/SwMedia3D.vue +4 -2
- package/{components → app/components}/SwNewsletterForm.vue +45 -34
- package/{components → app/components}/SwPagination.vue +3 -5
- package/{components → app/components}/SwProductAddToCart.vue +22 -27
- package/app/components/SwProductCard.vue +169 -0
- package/app/components/SwProductCardDetails.vue +74 -0
- package/app/components/SwProductCardImage.vue +90 -0
- package/app/components/SwProductCardSkeleton.vue +33 -0
- package/app/components/SwProductGallery.vue +43 -0
- package/app/components/SwProductListingFilter.vue +75 -0
- package/app/components/SwProductListingFilters.vue +304 -0
- package/app/components/SwProductListingFiltersHorizontal.vue +306 -0
- package/{components → app/components}/SwProductPrice.vue +3 -3
- package/app/components/SwProductRating.vue +40 -0
- package/{components → app/components}/SwProductReviews.vue +25 -23
- package/app/components/SwProductReviewsForm.vue +292 -0
- package/{components → app/components}/SwProductUnits.vue +10 -15
- package/app/components/SwQuantitySelect.vue +103 -0
- package/{components → app/components}/SwSlider.vue +154 -55
- package/app/components/SwSortDropdown.vue +87 -0
- package/app/components/SwStockInfo.vue +44 -0
- package/{components → app/components}/SwVariantConfigurator.vue +13 -12
- package/app/components/listing-filters/SwFilterPrice.vue +219 -0
- package/app/components/listing-filters/SwFilterProperties.vue +120 -0
- package/app/components/listing-filters/SwFilterRating.vue +99 -0
- package/app/components/listing-filters/SwFilterShippingFree.vue +114 -0
- package/app/components/public/cms/CmsBlockSpatialViewer.vue +94 -0
- package/app/components/public/cms/CmsGenericBlock.md +42 -0
- package/{components → app/components}/public/cms/CmsGenericBlock.vue +15 -1
- package/{components → app/components}/public/cms/CmsPage.md +19 -2
- package/{components → app/components}/public/cms/CmsPage.vue +30 -5
- package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +1 -1
- package/{components → app/components}/public/cms/block/CmsBlockGalleryBuybox.vue +5 -5
- package/{components → app/components}/public/cms/block/CmsBlockImageBubbleRow.vue +5 -5
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +41 -0
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +42 -0
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +37 -0
- package/{components → app/components}/public/cms/block/CmsBlockImageSimpleGrid.vue +11 -5
- package/{components → app/components}/public/cms/block/CmsBlockImageText.vue +7 -3
- package/{components → app/components}/public/cms/block/CmsBlockImageTextBubble.vue +13 -16
- package/{components → app/components}/public/cms/block/CmsBlockImageTextCover.vue +7 -9
- package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +88 -0
- package/app/components/public/cms/block/CmsBlockImageTextRow.vue +53 -0
- package/{components → app/components}/public/cms/block/CmsBlockImageThreeColumn.vue +10 -4
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +37 -0
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +37 -0
- package/{components → app/components}/public/cms/block/CmsBlockProductHeading.vue +1 -1
- package/{components → app/components}/public/cms/block/CmsBlockProductThreeColumn.vue +10 -4
- package/{components → app/components}/public/cms/block/CmsBlockSidebarFilter.vue +3 -1
- package/{components → app/components}/public/cms/block/CmsBlockTextOnImage.vue +8 -5
- package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
- package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
- package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +22 -6
- package/{components → app/components}/public/cms/element/CmsElementImage.vue +58 -21
- package/app/components/public/cms/element/CmsElementImageGallery.vue +225 -0
- package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
- package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +8 -1
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
- package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +31 -95
- package/app/components/public/cms/element/CmsElementProductName.vue +16 -0
- package/app/components/public/cms/element/CmsElementProductSlider.vue +101 -0
- package/app/components/public/cms/element/CmsElementSidebarFilter.vue +20 -0
- package/{components → app/components}/public/cms/element/CmsElementText.vue +17 -12
- package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
- package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +2 -2
- package/app/components/public/cms/section/CmsSectionSidebar.vue +39 -0
- package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
- package/app/components/ui/BaseButton.vue +102 -0
- package/app/components/ui/BaseIcon.vue +15 -0
- package/app/components/ui/Checkbox.vue +49 -0
- package/app/components/ui/CheckmarkIcon.vue +23 -0
- package/app/components/ui/ChevronIcon.vue +34 -0
- package/app/components/ui/ExclamationIcon.vue +11 -0
- package/app/components/ui/IconButton.vue +32 -0
- package/app/components/ui/RadioButton.vue +26 -0
- package/app/components/ui/StarIcon.vue +18 -0
- package/app/components/ui/SwitchButton.vue +100 -0
- package/app/components/ui/UserIcon.vue +11 -0
- package/app/components/ui/WishlistIcon.vue +15 -0
- package/app/composables/useImagePlaceholder.ts +27 -0
- package/app/composables/useLcpImagePreload.test.ts +229 -0
- package/app/composables/useLcpImagePreload.ts +39 -0
- package/{helpers → app/helpers}/clientOnly.ts +5 -0
- package/app/helpers/cms/findFirstCmsImageUrl.ts +86 -0
- package/app/helpers/cms/getImageSizes.test.ts +50 -0
- package/app/helpers/cms/getImageSizes.ts +36 -0
- package/app/helpers/html-to-vue/ast.ts +106 -0
- package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +1 -1
- package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +7 -11
- package/app/helpers/html-to-vue/renderer.ts +116 -0
- package/app/plugins/unocss-runtime.client.ts +23 -0
- package/app/providers/shopware.test.ts +213 -0
- package/app/providers/shopware.ts +107 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +2 -2
- package/index.d.ts +36 -0
- package/nuxt.config.ts +100 -6
- package/package.json +33 -23
- package/uno.config.ts +94 -0
- package/components/SwCategoryNavigation.vue +0 -44
- package/components/SwCategoryNavigationLink.vue +0 -57
- package/components/SwListingProductPrice.vue +0 -89
- package/components/SwProductCard.vue +0 -286
- package/components/SwProductGallery.vue +0 -39
- package/components/SwProductListingFilter.vue +0 -42
- package/components/SwProductListingFilters.vue +0 -292
- package/components/listing-filters/SwFilterPrice.vue +0 -160
- package/components/listing-filters/SwFilterProperties.vue +0 -123
- package/components/listing-filters/SwFilterRating.vue +0 -101
- package/components/listing-filters/SwFilterShippingFree.vue +0 -104
- package/components/public/cms/CmsGenericBlock.md +0 -27
- package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
- package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
- package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
- package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
- package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
- package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
- package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
- package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
- package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
- package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
- package/components/public/cms/element/CmsElementProductName.vue +0 -10
- package/components/public/cms/element/CmsElementProductSlider.vue +0 -80
- package/components/public/cms/element/CmsElementSidebarFilter.vue +0 -12
- package/components/public/cms/section/CmsSectionSidebar.vue +0 -41
- package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
- package/helpers/html-to-vue/ast.ts +0 -72
- package/helpers/html-to-vue/renderer.ts +0 -56
- /package/{components → app/components}/SwSharedPrice.vue +0 -0
- /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
- /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
- /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockHtml.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +0 -0
- /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
- /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
- /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
- /package/{helpers → app/helpers}/media/isSpatial.ts +0 -0
|
@@ -10,65 +10,83 @@ import {
|
|
|
10
10
|
useTemplateRef,
|
|
11
11
|
watch,
|
|
12
12
|
} from "vue";
|
|
13
|
-
import type { CSSProperties, VNodeArrayChildren } from "vue";
|
|
14
|
-
import { useCmsElementConfig } from "#imports";
|
|
13
|
+
import type { CSSProperties, VNode, VNodeArrayChildren } from "vue";
|
|
14
|
+
import { useCmsElementConfig, useHead, useId } from "#imports";
|
|
15
15
|
import type { Schemas } from "#shopware";
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
const {
|
|
18
|
+
config,
|
|
19
|
+
slidesToShow: slidesToShowProp = 1,
|
|
20
|
+
slidesToScroll: slidesToScrollProp = 1,
|
|
21
|
+
gap = "0px",
|
|
22
|
+
autoplay = false,
|
|
23
|
+
autoplaySpeed = 3000,
|
|
24
|
+
ssrBreakpoints,
|
|
25
|
+
} = defineProps<{
|
|
26
|
+
config: SliderElementConfig;
|
|
27
|
+
slidesToShow?: number;
|
|
28
|
+
slidesToScroll?: number;
|
|
29
|
+
gap?: string;
|
|
30
|
+
autoplay?: boolean;
|
|
31
|
+
autoplaySpeed?: number;
|
|
32
|
+
/** CSS media query breakpoints for responsive SSR layout.
|
|
33
|
+
* Keys are media queries, values are number of visible slides.
|
|
34
|
+
* e.g. { '(min-width: 768px)': 2, '(min-width: 1280px)': 4 }
|
|
35
|
+
* Base case (mobile) defaults to 1 visible slide. */
|
|
36
|
+
ssrBreakpoints?: Record<string, number>;
|
|
37
|
+
}>();
|
|
38
|
+
|
|
39
|
+
const sliderId = useId();
|
|
34
40
|
|
|
35
41
|
const { getConfigValue } = useCmsElementConfig({
|
|
36
|
-
config:
|
|
37
|
-
} as Schemas["CmsSlot"] & {
|
|
42
|
+
config: config,
|
|
43
|
+
} as Omit<Schemas["CmsSlot"], "config"> & {
|
|
38
44
|
config: SliderElementConfig;
|
|
39
45
|
});
|
|
40
46
|
|
|
41
47
|
const slots = useSlots() as {
|
|
42
48
|
default?: () => { children: VNodeArrayChildren }[];
|
|
43
49
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
50
|
+
|
|
51
|
+
// get fresh children from slot - call this each time to get new VNode instances
|
|
52
|
+
function getSlotChildren(): VNode[] {
|
|
53
|
+
return (slots?.default?.()[0]?.children as VNode[]) ?? [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const childrenRaw = computed(() => getSlotChildren());
|
|
57
|
+
|
|
47
58
|
const slidesToScroll = computed(() =>
|
|
48
|
-
|
|
49
|
-
?
|
|
50
|
-
:
|
|
59
|
+
slidesToScrollProp >= slidesToShowProp
|
|
60
|
+
? slidesToShowProp
|
|
61
|
+
: slidesToScrollProp,
|
|
51
62
|
);
|
|
52
63
|
const slidesToShow = computed(() =>
|
|
53
|
-
|
|
64
|
+
slidesToShowProp >= childrenRaw.value.length
|
|
54
65
|
? childrenRaw.value.length
|
|
55
|
-
:
|
|
66
|
+
: slidesToShowProp,
|
|
56
67
|
);
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
|
|
69
|
+
// build children array with fresh VNodes for infinite scroll
|
|
70
|
+
// we must call getSlotChildren() separately for each section because Vue can only render each VNode once
|
|
71
|
+
const children = computed<VNode[]>(() => {
|
|
72
|
+
const count = childrenRaw.value.length;
|
|
73
|
+
if (count === 0) return [];
|
|
74
|
+
|
|
75
|
+
const n = slidesToShow.value;
|
|
59
76
|
return [
|
|
60
|
-
...
|
|
61
|
-
...
|
|
62
|
-
...
|
|
63
|
-
]
|
|
77
|
+
...getSlotChildren().slice(-n), // prepend: last N slides
|
|
78
|
+
...getSlotChildren(), // main slides
|
|
79
|
+
...getSlotChildren().slice(0, n), // append: first N slides
|
|
80
|
+
];
|
|
64
81
|
});
|
|
82
|
+
|
|
65
83
|
const emit = defineEmits<(e: "changeSlide", index: number) => void>();
|
|
66
|
-
const slider = useTemplateRef("slider");
|
|
67
|
-
const imageSlider = useTemplateRef("imageSlider");
|
|
84
|
+
const slider = useTemplateRef<HTMLDivElement>("slider");
|
|
85
|
+
const imageSlider = useTemplateRef<HTMLDivElement>("imageSlider");
|
|
68
86
|
const imageSliderTrackStyle = ref<CSSProperties>();
|
|
69
87
|
const activeSlideIndex = ref<number>(0);
|
|
70
88
|
const speed = ref<number>(300);
|
|
71
|
-
const imageSliderTrack = useTemplateRef("imageSliderTrack");
|
|
89
|
+
const imageSliderTrack = useTemplateRef<HTMLDivElement>("imageSliderTrack");
|
|
72
90
|
const autoPlayInterval = ref();
|
|
73
91
|
const isReady = ref<boolean>();
|
|
74
92
|
const isSliding = ref<boolean>();
|
|
@@ -76,6 +94,79 @@ const isSliding = ref<boolean>();
|
|
|
76
94
|
const { width: imageSliderWidth } = useElementSize(imageSlider);
|
|
77
95
|
let timeoutGuard: ReturnType<typeof setTimeout> | undefined;
|
|
78
96
|
|
|
97
|
+
// SSR-safe fallback so the first slide is visible before JS hydrates
|
|
98
|
+
const ssrTrackStyle = computed<CSSProperties>(() => {
|
|
99
|
+
const total = children.value.length;
|
|
100
|
+
const n = slidesToShow.value;
|
|
101
|
+
if (total === 0 || n === 0) return {};
|
|
102
|
+
|
|
103
|
+
// Transform is constant: always skip N prepended clones
|
|
104
|
+
const transform = `translateX(-${(n / total) * 100}%)`;
|
|
105
|
+
|
|
106
|
+
if (ssrBreakpoints) {
|
|
107
|
+
// Both width and transform handled by CSS media queries via useHead
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
width: `${(total / n) * 100}%`,
|
|
113
|
+
transform,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Inject responsive CSS into <head> for SSR breakpoints.
|
|
118
|
+
// Transform is constant: always skip N prepended clones (`n / total`),
|
|
119
|
+
// because translateX(%) is relative to the element's own width which scales
|
|
120
|
+
// proportionally with the number of visible slides. Only width varies per breakpoint.
|
|
121
|
+
// Removed entirely once client-side JS sets imageSliderTrackStyle.
|
|
122
|
+
useHead(
|
|
123
|
+
computed(() => {
|
|
124
|
+
if (!ssrBreakpoints || imageSliderTrackStyle.value) return {};
|
|
125
|
+
const total = children.value.length;
|
|
126
|
+
const n = slidesToShow.value;
|
|
127
|
+
if (total === 0 || n === 0) return {};
|
|
128
|
+
|
|
129
|
+
const sel = `[data-ssr-slider="${sliderId}"]`;
|
|
130
|
+
const tx = `translateX(-${(n / total) * 100}%)`;
|
|
131
|
+
|
|
132
|
+
// Mobile base: 1 slide visible
|
|
133
|
+
let css = `${sel}{width:${total * 100}%;transform:${tx}}`;
|
|
134
|
+
// Breakpoint overrides — only width changes
|
|
135
|
+
for (const [query, slides] of Object.entries(ssrBreakpoints)) {
|
|
136
|
+
css += `@media ${query}{${sel}{width:${(total / slides) * 100}%}}`;
|
|
137
|
+
}
|
|
138
|
+
return { style: [{ innerHTML: css }] };
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Touch event handling for mobile swipe gestures
|
|
143
|
+
const touchStartX = ref(0);
|
|
144
|
+
const touchEndX = ref(0);
|
|
145
|
+
|
|
146
|
+
function onTouchStart(event: TouchEvent) {
|
|
147
|
+
touchStartX.value = event.touches?.[0]?.clientX || 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function onTouchMove(event: TouchEvent) {
|
|
151
|
+
touchEndX.value = event.touches?.[0]?.clientX || 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function onTouchEnd() {
|
|
155
|
+
const deltaX = touchEndX.value - touchStartX.value;
|
|
156
|
+
const threshold = 50; // pixels
|
|
157
|
+
|
|
158
|
+
if (Math.abs(deltaX) > threshold) {
|
|
159
|
+
if (deltaX < 0) {
|
|
160
|
+
next();
|
|
161
|
+
} else {
|
|
162
|
+
previous();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
touchStartX.value = 0;
|
|
167
|
+
touchEndX.value = 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
79
170
|
onMounted(() => {
|
|
80
171
|
initSlider();
|
|
81
172
|
|
|
@@ -92,12 +183,12 @@ onBeforeUnmount(() => {
|
|
|
92
183
|
});
|
|
93
184
|
|
|
94
185
|
watch(
|
|
95
|
-
() =>
|
|
186
|
+
() => autoplay && isReady.value,
|
|
96
187
|
(value) => {
|
|
97
188
|
if (value) {
|
|
98
189
|
autoPlayInterval.value = setInterval(() => {
|
|
99
190
|
next();
|
|
100
|
-
},
|
|
191
|
+
}, autoplaySpeed);
|
|
101
192
|
} else {
|
|
102
193
|
if (autoPlayInterval.value) {
|
|
103
194
|
clearInterval(autoPlayInterval.value);
|
|
@@ -112,8 +203,8 @@ watch(
|
|
|
112
203
|
const imageSliderStyle = computed(() => {
|
|
113
204
|
if (getConfigValue("displayMode") === "cover") {
|
|
114
205
|
return {
|
|
115
|
-
|
|
116
|
-
margin: `0 -${
|
|
206
|
+
minHeight: getConfigValue("minHeight"),
|
|
207
|
+
margin: `0 -${gap}`,
|
|
117
208
|
};
|
|
118
209
|
}
|
|
119
210
|
return {
|
|
@@ -129,10 +220,10 @@ const displayModeValue = computed(
|
|
|
129
220
|
);
|
|
130
221
|
|
|
131
222
|
const navigationArrowsValue = computed(
|
|
132
|
-
() =>
|
|
223
|
+
() => getConfigValue("navigationArrows") || "none",
|
|
133
224
|
);
|
|
134
225
|
const navigationDotsValue = computed(
|
|
135
|
-
() =>
|
|
226
|
+
() => getConfigValue("navigationDots") || "none",
|
|
136
227
|
);
|
|
137
228
|
|
|
138
229
|
function initSlider() {
|
|
@@ -246,16 +337,19 @@ defineExpose({
|
|
|
246
337
|
'relative overflow-hidden h-full': true,
|
|
247
338
|
'px-10': navigationArrowsValue === 'outside',
|
|
248
339
|
'pb-15': navigationDotsValue === 'outside',
|
|
249
|
-
'opacity-0': !isReady,
|
|
250
340
|
}"
|
|
251
341
|
>
|
|
252
342
|
<div
|
|
253
343
|
ref="imageSlider"
|
|
254
344
|
class="overflow-hidden h-full"
|
|
255
345
|
:style="imageSliderStyle"
|
|
346
|
+
@touchstart="onTouchStart"
|
|
347
|
+
@touchmove="onTouchMove"
|
|
348
|
+
@touchend="onTouchEnd"
|
|
256
349
|
>
|
|
257
350
|
<div
|
|
258
351
|
ref="imageSliderTrack"
|
|
352
|
+
:data-ssr-slider="ssrBreakpoints ? sliderId : undefined"
|
|
259
353
|
:class="{
|
|
260
354
|
flex: true,
|
|
261
355
|
'items-center':
|
|
@@ -266,7 +360,7 @@ defineExpose({
|
|
|
266
360
|
'items-end':
|
|
267
361
|
displayModeValue === 'contain' && verticalAlignValue === 'flex-end',
|
|
268
362
|
}"
|
|
269
|
-
:style="imageSliderTrackStyle"
|
|
363
|
+
:style="imageSliderTrackStyle || ssrTrackStyle"
|
|
270
364
|
>
|
|
271
365
|
<div
|
|
272
366
|
v-for="(child, index) of children"
|
|
@@ -275,7 +369,7 @@ defineExpose({
|
|
|
275
369
|
:style="{
|
|
276
370
|
width: imageSliderWidth
|
|
277
371
|
? `${imageSliderWidth / slidesToShow}px`
|
|
278
|
-
:
|
|
372
|
+
: `${100 / children.length}%`,
|
|
279
373
|
padding: `0 ${gap}`,
|
|
280
374
|
height: displayModeValue === 'standard' ? 'min-content' : '100%',
|
|
281
375
|
}"
|
|
@@ -288,29 +382,33 @@ defineExpose({
|
|
|
288
382
|
<button
|
|
289
383
|
aria-label="Previous slide"
|
|
290
384
|
:class="{
|
|
291
|
-
'absolute
|
|
385
|
+
'absolute top-1/2 left-4 transform -translate-y-1/2 w-10 h-10 rounded-full flex items-center justify-center': true,
|
|
386
|
+
'bg-brand-tertiary text-surface-on-surface':
|
|
387
|
+
navigationArrowsValue === 'outside',
|
|
292
388
|
'transition bg-white/20 hover:bg-white/50':
|
|
293
389
|
navigationArrowsValue === 'inside',
|
|
294
390
|
}"
|
|
295
391
|
@click="previous"
|
|
296
392
|
>
|
|
297
|
-
<
|
|
393
|
+
<SwChevronIcon direction="left" />
|
|
298
394
|
</button>
|
|
299
395
|
<button
|
|
300
396
|
aria-label="Next slide"
|
|
301
397
|
:class="{
|
|
302
|
-
'absolute
|
|
398
|
+
'absolute top-1/2 right-4 transform -translate-y-1/2 w-10 h-10 rounded-full flex items-center justify-center': true,
|
|
399
|
+
'bg-brand-tertiary text-surface-on-surface':
|
|
400
|
+
navigationArrowsValue === 'outside',
|
|
303
401
|
'transition bg-white/20 hover:bg-white/50':
|
|
304
402
|
navigationArrowsValue === 'inside',
|
|
305
403
|
}"
|
|
306
404
|
@click="next"
|
|
307
405
|
>
|
|
308
|
-
<
|
|
406
|
+
<SwChevronIcon direction="right" />
|
|
309
407
|
</button>
|
|
310
408
|
</div>
|
|
311
409
|
<div
|
|
312
410
|
:class="{
|
|
313
|
-
'absolute bottom-5 left-1/2 transform -translate-x-1/2 gap-
|
|
411
|
+
'absolute bottom-5 left-1/2 transform -translate-x-1/2 gap-2 items-center': true,
|
|
314
412
|
flex: navigationDotsValue !== 'none',
|
|
315
413
|
hidden: navigationDotsValue === 'none',
|
|
316
414
|
}"
|
|
@@ -319,9 +417,10 @@ defineExpose({
|
|
|
319
417
|
v-for="(_, i) of childrenRaw"
|
|
320
418
|
:key="`dot-${i}`"
|
|
321
419
|
:class="{
|
|
322
|
-
'
|
|
323
|
-
'bg-
|
|
324
|
-
'bg-
|
|
420
|
+
'rounded-full cursor-pointer transition-all duration-300': true,
|
|
421
|
+
'w-6 h-2 bg-surface-on-surface-variant': i === activeSlideIndex,
|
|
422
|
+
'w-2 h-2 bg-surface-surface-container-highest':
|
|
423
|
+
i !== activeSlideIndex,
|
|
325
424
|
}"
|
|
326
425
|
@click="() => goToSlide(i)"
|
|
327
426
|
></div>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onClickOutside } from "@vueuse/core";
|
|
3
|
+
import { ref, useTemplateRef } from "vue";
|
|
4
|
+
|
|
5
|
+
type SortOption = {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string | null;
|
|
8
|
+
translated?: {
|
|
9
|
+
label: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
defineProps<{
|
|
14
|
+
sortOptions: SortOption[];
|
|
15
|
+
currentSort: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
"sort-change": [string];
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const isSortMenuOpen = ref(false);
|
|
24
|
+
const dropdownElement = useTemplateRef<HTMLDivElement>("dropdownElement");
|
|
25
|
+
|
|
26
|
+
onClickOutside(dropdownElement, () => {
|
|
27
|
+
isSortMenuOpen.value = false;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const handleSortingClick = (key: string) => {
|
|
31
|
+
emit("sort-change", key);
|
|
32
|
+
isSortMenuOpen.value = false;
|
|
33
|
+
};
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div ref="dropdownElement" class="flex items-center">
|
|
38
|
+
<div class="relative inline-block text-left">
|
|
39
|
+
<SwBaseButton
|
|
40
|
+
variant="ghost"
|
|
41
|
+
size="medium"
|
|
42
|
+
type="button"
|
|
43
|
+
@click="isSortMenuOpen = !isSortMenuOpen"
|
|
44
|
+
id="menu-button"
|
|
45
|
+
:aria-expanded="isSortMenuOpen"
|
|
46
|
+
aria-haspopup="true"
|
|
47
|
+
class="group pr-0"
|
|
48
|
+
>
|
|
49
|
+
<span class="inline-flex items-center gap-1">
|
|
50
|
+
{{ label }}
|
|
51
|
+
<SwChevronIcon
|
|
52
|
+
:direction="isSortMenuOpen ? 'up' : 'down'"
|
|
53
|
+
:size="24"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
focusable="false"
|
|
56
|
+
/>
|
|
57
|
+
</span>
|
|
58
|
+
</SwBaseButton>
|
|
59
|
+
<div
|
|
60
|
+
:class="[isSortMenuOpen ? 'absolute' : 'hidden']"
|
|
61
|
+
class="origin-top-right right-0 mt-2 w-40 rounded-md shadow-2xl bg-surface-surface ring-1 ring-outline-outline-variant focus:outline-none z-50"
|
|
62
|
+
role="menu"
|
|
63
|
+
aria-orientation="vertical"
|
|
64
|
+
aria-labelledby="menu-button"
|
|
65
|
+
tabindex="-1"
|
|
66
|
+
>
|
|
67
|
+
<div class="py-1" role="none">
|
|
68
|
+
<button
|
|
69
|
+
v-for="sorting in sortOptions"
|
|
70
|
+
:key="sorting.key"
|
|
71
|
+
@click="handleSortingClick(sorting.key)"
|
|
72
|
+
:class="[
|
|
73
|
+
sorting.key === currentSort
|
|
74
|
+
? 'font-medium text-surface-on-surface'
|
|
75
|
+
: 'text-surface-on-surface-variant',
|
|
76
|
+
]"
|
|
77
|
+
class="block w-full text-left px-4 py-2 text-sm bg-transparent hover:bg-surface-surface-container"
|
|
78
|
+
role="menuitem"
|
|
79
|
+
tabindex="-1"
|
|
80
|
+
>
|
|
81
|
+
{{ sorting.translated?.label }}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { defu } from "defu";
|
|
3
|
+
|
|
4
|
+
import { useCmsTranslations } from "#imports";
|
|
5
|
+
import type { Schemas } from "#shopware";
|
|
6
|
+
|
|
7
|
+
defineProps<{
|
|
8
|
+
availableStock: number;
|
|
9
|
+
minPurchase: number;
|
|
10
|
+
deliveryTime?: Schemas["DeliveryTime"];
|
|
11
|
+
restockTime?: number;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
type Translations = {
|
|
15
|
+
product: {
|
|
16
|
+
deliveryTime: string;
|
|
17
|
+
days: string;
|
|
18
|
+
noAvailable: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let translations: Translations = {
|
|
23
|
+
product: {
|
|
24
|
+
deliveryTime: "Available, delivery time",
|
|
25
|
+
days: "days",
|
|
26
|
+
noAvailable: "No longer available",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
31
|
+
</script>
|
|
32
|
+
<template>
|
|
33
|
+
<div class="inline-flex justify-start items-center gap-2">
|
|
34
|
+
<div class="w-2 h-2 bg-states-success rounded-full" v-if="availableStock > 0"></div>
|
|
35
|
+
<div class="w-2 h-2 bg-states-error rounded-full" v-else></div>
|
|
36
|
+
<span v-if="availableStock >= minPurchase && deliveryTime">{{ translations.product.deliveryTime }} {{
|
|
37
|
+
deliveryTime?.name }}
|
|
38
|
+
</span>
|
|
39
|
+
<span v-else-if="availableStock < minPurchase && deliveryTime && restockTime">
|
|
40
|
+
{{ translations.product.deliveryTime }} {{ restockTime }}
|
|
41
|
+
{{ translations.product.days }} {{ deliveryTime?.name }}</span>
|
|
42
|
+
<span v-else>{{ translations.product.noAvailable }}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
@@ -12,14 +12,9 @@ const { getUrlPrefix } = useUrlResolver();
|
|
|
12
12
|
|
|
13
13
|
const prefix = getUrlPrefix();
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}>(),
|
|
19
|
-
{
|
|
20
|
-
allowRedirect: true,
|
|
21
|
-
},
|
|
22
|
-
);
|
|
15
|
+
const { allowRedirect = true } = defineProps<{
|
|
16
|
+
allowRedirect?: boolean;
|
|
17
|
+
}>();
|
|
23
18
|
|
|
24
19
|
type Translations = {
|
|
25
20
|
product: {
|
|
@@ -61,7 +56,7 @@ const onHandleChange = async () => {
|
|
|
61
56
|
getProductRoute(variantFound),
|
|
62
57
|
prefix,
|
|
63
58
|
);
|
|
64
|
-
if (
|
|
59
|
+
if (allowRedirect && selectedOptionsVariantPath) {
|
|
65
60
|
try {
|
|
66
61
|
router.push(selectedOptionsVariantPath);
|
|
67
62
|
} catch {
|
|
@@ -90,7 +85,7 @@ const onHandleChange = async () => {
|
|
|
90
85
|
:key="optionGroup.id"
|
|
91
86
|
class="mt-6"
|
|
92
87
|
>
|
|
93
|
-
<
|
|
88
|
+
<div class="text-sm text-gray-900 font-medium">{{ optionGroup.name }}</div>
|
|
94
89
|
<fieldset class="mt-4 flex-1">
|
|
95
90
|
<legend class="sr-only">
|
|
96
91
|
{{ translations.product.chooseA }} {{ optionGroup.name }}
|
|
@@ -102,9 +97,15 @@ const onHandleChange = async () => {
|
|
|
102
97
|
data-testid="product-variant"
|
|
103
98
|
class="group relative border rounded-md py-3 px-4 flex items-center justify-center text-sm font-medium uppercase hover:bg-gray-50 focus:outline-none sm:flex-1 bg-white shadow-sm text-gray-900 cursor-pointer"
|
|
104
99
|
:class="{
|
|
105
|
-
'border-3 border-
|
|
100
|
+
'border-3 border-brand-primary': isOptionSelected(option.id),
|
|
106
101
|
}"
|
|
107
|
-
@click="
|
|
102
|
+
@click="
|
|
103
|
+
handleChange(
|
|
104
|
+
optionGroup.translated.name,
|
|
105
|
+
option.id,
|
|
106
|
+
onHandleChange,
|
|
107
|
+
)
|
|
108
|
+
"
|
|
108
109
|
>
|
|
109
110
|
<p
|
|
110
111
|
:id="`${option.id}-choice-label`"
|