@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.
Files changed (198) hide show
  1. package/README.md +398 -12
  2. package/app/app.config.ts +18 -0
  3. package/app/assets/icons/check-circle.svg +3 -0
  4. package/app/assets/icons/checkmark.svg +3 -0
  5. package/app/assets/icons/chevron.svg +3 -0
  6. package/app/assets/icons/exclamation-circle.svg +3 -0
  7. package/app/assets/icons/star-empty.svg +3 -0
  8. package/app/assets/icons/star-filled.svg +3 -0
  9. package/app/assets/icons/user.svg +1 -0
  10. package/app/components/SwCategoryNavigation.vue +83 -0
  11. package/app/components/SwCategoryNavigationLink.vue +128 -0
  12. package/{components → app/components}/SwContactForm.vue +27 -27
  13. package/app/components/SwFilterChips.vue +144 -0
  14. package/app/components/SwFilterDropdown.vue +54 -0
  15. package/app/components/SwListingProductPrice.vue +89 -0
  16. package/{components → app/components}/SwMedia3D.vue +4 -2
  17. package/{components → app/components}/SwNewsletterForm.vue +45 -34
  18. package/{components → app/components}/SwPagination.vue +3 -5
  19. package/{components → app/components}/SwProductAddToCart.vue +22 -27
  20. package/app/components/SwProductCard.vue +169 -0
  21. package/app/components/SwProductCardDetails.vue +74 -0
  22. package/app/components/SwProductCardImage.vue +90 -0
  23. package/app/components/SwProductCardSkeleton.vue +33 -0
  24. package/app/components/SwProductGallery.vue +43 -0
  25. package/app/components/SwProductListingFilter.vue +75 -0
  26. package/app/components/SwProductListingFilters.vue +304 -0
  27. package/app/components/SwProductListingFiltersHorizontal.vue +306 -0
  28. package/{components → app/components}/SwProductPrice.vue +3 -3
  29. package/app/components/SwProductRating.vue +40 -0
  30. package/{components → app/components}/SwProductReviews.vue +25 -23
  31. package/app/components/SwProductReviewsForm.vue +292 -0
  32. package/{components → app/components}/SwProductUnits.vue +10 -15
  33. package/app/components/SwQuantitySelect.vue +103 -0
  34. package/{components → app/components}/SwSlider.vue +154 -55
  35. package/app/components/SwSortDropdown.vue +87 -0
  36. package/app/components/SwStockInfo.vue +44 -0
  37. package/{components → app/components}/SwVariantConfigurator.vue +13 -12
  38. package/app/components/listing-filters/SwFilterPrice.vue +219 -0
  39. package/app/components/listing-filters/SwFilterProperties.vue +120 -0
  40. package/app/components/listing-filters/SwFilterRating.vue +99 -0
  41. package/app/components/listing-filters/SwFilterShippingFree.vue +114 -0
  42. package/app/components/public/cms/CmsBlockSpatialViewer.vue +94 -0
  43. package/app/components/public/cms/CmsGenericBlock.md +42 -0
  44. package/{components → app/components}/public/cms/CmsGenericBlock.vue +15 -1
  45. package/{components → app/components}/public/cms/CmsPage.md +19 -2
  46. package/{components → app/components}/public/cms/CmsPage.vue +30 -5
  47. package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +1 -1
  48. package/{components → app/components}/public/cms/block/CmsBlockGalleryBuybox.vue +5 -5
  49. package/{components → app/components}/public/cms/block/CmsBlockImageBubbleRow.vue +5 -5
  50. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +41 -0
  51. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +42 -0
  52. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +37 -0
  53. package/{components → app/components}/public/cms/block/CmsBlockImageSimpleGrid.vue +11 -5
  54. package/{components → app/components}/public/cms/block/CmsBlockImageText.vue +7 -3
  55. package/{components → app/components}/public/cms/block/CmsBlockImageTextBubble.vue +13 -16
  56. package/{components → app/components}/public/cms/block/CmsBlockImageTextCover.vue +7 -9
  57. package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +88 -0
  58. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +53 -0
  59. package/{components → app/components}/public/cms/block/CmsBlockImageThreeColumn.vue +10 -4
  60. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +37 -0
  61. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +37 -0
  62. package/{components → app/components}/public/cms/block/CmsBlockProductHeading.vue +1 -1
  63. package/{components → app/components}/public/cms/block/CmsBlockProductThreeColumn.vue +10 -4
  64. package/{components → app/components}/public/cms/block/CmsBlockSidebarFilter.vue +3 -1
  65. package/{components → app/components}/public/cms/block/CmsBlockTextOnImage.vue +8 -5
  66. package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
  67. package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
  68. package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +22 -6
  69. package/{components → app/components}/public/cms/element/CmsElementImage.vue +58 -21
  70. package/app/components/public/cms/element/CmsElementImageGallery.vue +225 -0
  71. package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
  72. package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +8 -1
  73. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
  74. package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +31 -95
  75. package/app/components/public/cms/element/CmsElementProductName.vue +16 -0
  76. package/app/components/public/cms/element/CmsElementProductSlider.vue +101 -0
  77. package/app/components/public/cms/element/CmsElementSidebarFilter.vue +20 -0
  78. package/{components → app/components}/public/cms/element/CmsElementText.vue +17 -12
  79. package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
  80. package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +2 -2
  81. package/app/components/public/cms/section/CmsSectionSidebar.vue +39 -0
  82. package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
  83. package/app/components/ui/BaseButton.vue +102 -0
  84. package/app/components/ui/BaseIcon.vue +15 -0
  85. package/app/components/ui/Checkbox.vue +49 -0
  86. package/app/components/ui/CheckmarkIcon.vue +23 -0
  87. package/app/components/ui/ChevronIcon.vue +34 -0
  88. package/app/components/ui/ExclamationIcon.vue +11 -0
  89. package/app/components/ui/IconButton.vue +32 -0
  90. package/app/components/ui/RadioButton.vue +26 -0
  91. package/app/components/ui/StarIcon.vue +18 -0
  92. package/app/components/ui/SwitchButton.vue +100 -0
  93. package/app/components/ui/UserIcon.vue +11 -0
  94. package/app/components/ui/WishlistIcon.vue +15 -0
  95. package/app/composables/useImagePlaceholder.ts +27 -0
  96. package/app/composables/useLcpImagePreload.test.ts +229 -0
  97. package/app/composables/useLcpImagePreload.ts +39 -0
  98. package/{helpers → app/helpers}/clientOnly.ts +5 -0
  99. package/app/helpers/cms/findFirstCmsImageUrl.ts +86 -0
  100. package/app/helpers/cms/getImageSizes.test.ts +50 -0
  101. package/app/helpers/cms/getImageSizes.ts +36 -0
  102. package/app/helpers/html-to-vue/ast.ts +106 -0
  103. package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +1 -1
  104. package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +7 -11
  105. package/app/helpers/html-to-vue/renderer.ts +116 -0
  106. package/app/plugins/unocss-runtime.client.ts +23 -0
  107. package/app/providers/shopware.test.ts +213 -0
  108. package/app/providers/shopware.ts +107 -0
  109. package/dist/index.d.mts +3 -3
  110. package/dist/index.d.ts +3 -3
  111. package/dist/index.mjs +2 -2
  112. package/index.d.ts +36 -0
  113. package/nuxt.config.ts +100 -6
  114. package/package.json +33 -23
  115. package/uno.config.ts +94 -0
  116. package/components/SwCategoryNavigation.vue +0 -44
  117. package/components/SwCategoryNavigationLink.vue +0 -57
  118. package/components/SwListingProductPrice.vue +0 -89
  119. package/components/SwProductCard.vue +0 -286
  120. package/components/SwProductGallery.vue +0 -39
  121. package/components/SwProductListingFilter.vue +0 -42
  122. package/components/SwProductListingFilters.vue +0 -292
  123. package/components/listing-filters/SwFilterPrice.vue +0 -160
  124. package/components/listing-filters/SwFilterProperties.vue +0 -123
  125. package/components/listing-filters/SwFilterRating.vue +0 -101
  126. package/components/listing-filters/SwFilterShippingFree.vue +0 -104
  127. package/components/public/cms/CmsGenericBlock.md +0 -27
  128. package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
  129. package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
  130. package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
  131. package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
  132. package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
  133. package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
  134. package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
  135. package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
  136. package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
  137. package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
  138. package/components/public/cms/element/CmsElementProductName.vue +0 -10
  139. package/components/public/cms/element/CmsElementProductSlider.vue +0 -80
  140. package/components/public/cms/element/CmsElementSidebarFilter.vue +0 -12
  141. package/components/public/cms/section/CmsSectionSidebar.vue +0 -41
  142. package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
  143. package/helpers/html-to-vue/ast.ts +0 -72
  144. package/helpers/html-to-vue/renderer.ts +0 -56
  145. /package/{components → app/components}/SwSharedPrice.vue +0 -0
  146. /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
  147. /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
  148. /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
  149. /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
  150. /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
  151. /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
  152. /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
  153. /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
  154. /package/{components → app/components}/public/cms/block/CmsBlockHtml.vue +0 -0
  155. /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
  156. /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
  157. /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
  158. /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
  159. /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
  160. /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
  161. /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
  162. /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
  163. /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
  164. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
  165. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +0 -0
  166. /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
  167. /package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +0 -0
  168. /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
  169. /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
  170. /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
  171. /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
  172. /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
  173. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
  174. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
  175. /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
  176. /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
  177. /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
  178. /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
  179. /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
  180. /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
  181. /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
  182. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
  183. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
  184. /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
  185. /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
  186. /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
  187. /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
  188. /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
  189. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
  190. /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
  191. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
  192. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
  193. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
  194. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +0 -0
  195. /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
  196. /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
  197. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
  198. /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 props = withDefaults(
18
- defineProps<{
19
- config: SliderElementConfig;
20
- slidesToShow?: number;
21
- slidesToScroll?: number;
22
- gap?: string;
23
- autoplay?: boolean;
24
- autoplaySpeed?: number;
25
- }>(),
26
- {
27
- slidesToShow: 1,
28
- slidesToScroll: 1,
29
- gap: "0px",
30
- autoplay: false,
31
- autoplaySpeed: 3000,
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: props.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
- const childrenRaw = computed(
45
- () => (slots?.default?.()[0]?.children as VNodeArrayChildren) ?? [],
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
- props.slidesToScroll >= props.slidesToShow
49
- ? props.slidesToShow
50
- : props.slidesToScroll,
59
+ slidesToScrollProp >= slidesToShowProp
60
+ ? slidesToShowProp
61
+ : slidesToScrollProp,
51
62
  );
52
63
  const slidesToShow = computed(() =>
53
- props.slidesToShow >= childrenRaw.value.length
64
+ slidesToShowProp >= childrenRaw.value.length
54
65
  ? childrenRaw.value.length
55
- : props.slidesToShow,
66
+ : slidesToShowProp,
56
67
  );
57
- const children = computed<string[]>(() => {
58
- if (childrenRaw.value.length === 0) return [];
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
- ...childrenRaw.value.slice(-slidesToShow.value),
61
- ...childrenRaw.value,
62
- ...childrenRaw.value.slice(0, slidesToShow.value),
63
- ] as string[];
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
- () => props.autoplay && isReady.value,
186
+ () => autoplay && isReady.value,
96
187
  (value) => {
97
188
  if (value) {
98
189
  autoPlayInterval.value = setInterval(() => {
99
190
  next();
100
- }, props.autoplaySpeed);
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
- height: getConfigValue("minHeight"),
116
- margin: `0 -${props.gap}`,
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
- () => props.config?.navigationArrows?.value || "none",
223
+ () => getConfigValue("navigationArrows") || "none",
133
224
  );
134
225
  const navigationDotsValue = computed(
135
- () => props.config?.navigationDots?.value || "none",
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
- : 'auto',
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 bg-transparent top-1/2 left-0 transform -translate-y-1/2 py-4': true,
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
- <div class="w-15 h-15 i-carbon-chevron-left"></div>
393
+ <SwChevronIcon direction="left" />
298
394
  </button>
299
395
  <button
300
396
  aria-label="Next slide"
301
397
  :class="{
302
- 'absolute bg-transparent top-1/2 right-0 transform -translate-y-1/2 py-4': true,
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
- <div class="w-15 h-15 i-carbon-chevron-right"></div>
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-5': true,
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
- 'w-5 h-5 rounded-full cursor-pointer': true,
323
- 'bg-gray-100': i === activeSlideIndex,
324
- 'bg-gray-500/50': i !== activeSlideIndex,
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 props = withDefaults(
16
- defineProps<{
17
- allowRedirect?: boolean;
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 (props.allowRedirect && selectedOptionsVariantPath) {
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
- <h3 class="text-sm text-gray-900 font-medium">{{ optionGroup.name }}</h3>
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-indigo-600': isOptionSelected(option.id),
100
+ 'border-3 border-brand-primary': isOptionSelected(option.id),
106
101
  }"
107
- @click="handleChange(optionGroup.translated.name, option.id, onHandleChange)"
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`"