@shopware/cms-base-layer 1.5.0 → 2.0.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 (184) hide show
  1. package/README.md +328 -13
  2. package/app/app.config.ts +7 -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 +76 -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/SwListingProductPrice.vue +89 -0
  15. package/{components → app/components}/SwNewsletterForm.vue +45 -34
  16. package/{components → app/components}/SwPagination.vue +3 -5
  17. package/{components → app/components}/SwProductAddToCart.vue +22 -27
  18. package/app/components/SwProductCard.vue +170 -0
  19. package/app/components/SwProductCardDetails.vue +57 -0
  20. package/app/components/SwProductCardImage.vue +87 -0
  21. package/app/components/SwProductCardSkeleton.vue +33 -0
  22. package/app/components/SwProductListingFilter.vue +64 -0
  23. package/app/components/SwProductListingFilters.vue +308 -0
  24. package/{components → app/components}/SwProductReviews.vue +28 -13
  25. package/app/components/SwProductReviewsForm.vue +292 -0
  26. package/app/components/SwQuantitySelect.vue +106 -0
  27. package/{components → app/components}/SwSlider.vue +4 -4
  28. package/app/components/SwSortDropdown.vue +83 -0
  29. package/app/components/SwStockInfo.vue +44 -0
  30. package/{components → app/components}/SwVariantConfigurator.vue +1 -1
  31. package/app/components/listing-filters/SwFilterPrice.vue +214 -0
  32. package/app/components/listing-filters/SwFilterProperties.vue +113 -0
  33. package/app/components/listing-filters/SwFilterRating.vue +90 -0
  34. package/app/components/listing-filters/SwFilterShippingFree.vue +107 -0
  35. package/{components → app/components}/public/cms/CmsPage.vue +19 -4
  36. package/{components → app/components}/public/cms/block/CmsBlockGalleryBuybox.vue +5 -5
  37. package/{components → app/components}/public/cms/block/CmsBlockImageBubbleRow.vue +5 -5
  38. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +41 -0
  39. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +42 -0
  40. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +37 -0
  41. package/{components → app/components}/public/cms/block/CmsBlockImageSimpleGrid.vue +11 -5
  42. package/{components → app/components}/public/cms/block/CmsBlockImageText.vue +7 -3
  43. package/{components → app/components}/public/cms/block/CmsBlockImageTextBubble.vue +13 -16
  44. package/{components → app/components}/public/cms/block/CmsBlockImageTextCover.vue +7 -9
  45. package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +88 -0
  46. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +53 -0
  47. package/{components → app/components}/public/cms/block/CmsBlockImageThreeColumn.vue +10 -4
  48. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +37 -0
  49. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +37 -0
  50. package/{components → app/components}/public/cms/block/CmsBlockProductHeading.vue +1 -1
  51. package/{components → app/components}/public/cms/block/CmsBlockProductThreeColumn.vue +10 -4
  52. package/{components → app/components}/public/cms/block/CmsBlockSidebarFilter.vue +3 -1
  53. package/app/components/public/cms/block/CmsBlockTextOnImage.vue +30 -0
  54. package/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +4 -4
  55. package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +3 -5
  56. package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
  57. package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
  58. package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +3 -3
  59. package/{components → app/components}/public/cms/element/CmsElementImage.vue +52 -13
  60. package/app/components/public/cms/element/CmsElementImageGallery.vue +158 -0
  61. package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
  62. package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +2 -1
  63. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
  64. package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +23 -94
  65. package/app/components/public/cms/element/CmsElementProductName.vue +11 -0
  66. package/{components → app/components}/public/cms/element/CmsElementProductSlider.vue +4 -4
  67. package/{components → app/components}/public/cms/element/CmsElementText.vue +8 -2
  68. package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +8 -2
  69. package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
  70. package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +1 -1
  71. package/app/components/public/cms/section/CmsSectionSidebar.vue +36 -0
  72. package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
  73. package/app/components/ui/BaseButton.vue +99 -0
  74. package/app/components/ui/BaseIcon.vue +15 -0
  75. package/app/components/ui/Checkbox.vue +49 -0
  76. package/app/components/ui/CheckmarkIcon.vue +23 -0
  77. package/app/components/ui/ChevronIcon.vue +37 -0
  78. package/app/components/ui/ExclamationIcon.vue +11 -0
  79. package/app/components/ui/IconButton.vue +32 -0
  80. package/app/components/ui/RadioButton.vue +26 -0
  81. package/app/components/ui/StarIcon.vue +18 -0
  82. package/app/components/ui/SwitchButton.vue +100 -0
  83. package/app/components/ui/UserIcon.vue +11 -0
  84. package/app/components/ui/WishlistIcon.vue +20 -0
  85. package/app/composables/useImagePlaceholder.ts +27 -0
  86. package/{helpers → app/helpers}/clientOnly.ts +5 -0
  87. package/app/providers/shopware.test.ts +213 -0
  88. package/app/providers/shopware.ts +107 -0
  89. package/dist/index.d.mts +3 -3
  90. package/dist/index.d.ts +3 -3
  91. package/dist/index.mjs +2 -2
  92. package/index.d.ts +12 -0
  93. package/nuxt.config.ts +80 -6
  94. package/package.json +29 -21
  95. package/uno.config.ts +83 -0
  96. package/components/SwCategoryNavigation.vue +0 -44
  97. package/components/SwCategoryNavigationLink.vue +0 -57
  98. package/components/SwListingProductPrice.vue +0 -89
  99. package/components/SwProductCard.vue +0 -286
  100. package/components/SwProductListingFilter.vue +0 -42
  101. package/components/SwProductListingFilters.vue +0 -292
  102. package/components/listing-filters/SwFilterPrice.vue +0 -160
  103. package/components/listing-filters/SwFilterProperties.vue +0 -123
  104. package/components/listing-filters/SwFilterRating.vue +0 -101
  105. package/components/listing-filters/SwFilterShippingFree.vue +0 -104
  106. package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
  107. package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
  108. package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
  109. package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
  110. package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
  111. package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
  112. package/components/public/cms/block/CmsBlockTextOnImage.vue +0 -20
  113. package/components/public/cms/element/CmsBlockHtml.md +0 -1
  114. package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
  115. package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
  116. package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
  117. package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
  118. package/components/public/cms/element/CmsElementProductName.vue +0 -10
  119. package/components/public/cms/section/CmsSectionSidebar.vue +0 -49
  120. package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
  121. /package/{components → app/components}/SwMedia3D.vue +0 -0
  122. /package/{components → app/components}/SwProductGallery.vue +0 -0
  123. /package/{components → app/components}/SwProductPrice.vue +0 -0
  124. /package/{components → app/components}/SwProductUnits.vue +0 -0
  125. /package/{components → app/components}/SwSharedPrice.vue +0 -0
  126. /package/{components → app/components}/public/cms/CmsGenericBlock.md +0 -0
  127. /package/{components → app/components}/public/cms/CmsGenericBlock.vue +0 -0
  128. /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
  129. /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
  130. /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
  131. /package/{components → app/components}/public/cms/CmsPage.md +0 -0
  132. /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
  133. /package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +0 -0
  134. /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
  135. /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
  136. /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
  137. /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
  138. /package/{components/public/cms/element → app/components/public/cms/block}/CmsBlockHtml.vue +0 -0
  139. /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
  140. /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
  141. /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
  142. /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
  143. /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
  144. /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
  145. /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
  146. /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
  147. /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
  148. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
  149. /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
  150. /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
  151. /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
  152. /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
  153. /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
  154. /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
  155. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
  156. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
  157. /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
  158. /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
  159. /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
  160. /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
  161. /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
  162. /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
  163. /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
  164. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
  165. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
  166. /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
  167. /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
  168. /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
  169. /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
  170. /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
  171. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
  172. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.vue +0 -0
  173. /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
  174. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
  175. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
  176. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
  177. /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
  178. /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
  179. /package/{helpers → app/helpers}/html-to-vue/ast.ts +0 -0
  180. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
  181. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +0 -0
  182. /package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +0 -0
  183. /package/{helpers → app/helpers}/html-to-vue/renderer.ts +0 -0
  184. /package/{helpers → app/helpers}/media/isSpatial.ts +0 -0
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ import type { CmsBlockImageTwoColumn } from "@shopware/composables";
3
+ import { useCmsBlock } from "#imports";
4
+
5
+ const props = defineProps<{
6
+ content: CmsBlockImageTwoColumn;
7
+ }>();
8
+
9
+ const { getSlotContent } = useCmsBlock(props.content);
10
+
11
+ const leftContent = getSlotContent("left");
12
+ const rightContent = getSlotContent("right");
13
+ </script>
14
+ <template>
15
+ <div class="cms-block-image-two-column flex flex-col md:flex-row justify-start items-start gap-6 w-full">
16
+ <div class="w-full md:flex-1 md:min-w-0">
17
+ <CmsGenericElement :content="leftContent" />
18
+ </div>
19
+ <div class="w-full md:flex-1 md:min-w-0">
20
+ <CmsGenericElement :content="rightContent" />
21
+ </div>
22
+ </div>
23
+ </template>
24
+
25
+ <style scoped>
26
+ .cms-block-image-two-column :deep(.cms-element-image) {
27
+ @apply relative h-full w-full;
28
+ }
29
+
30
+ .cms-block-image-two-column :deep(.cms-element-image img) {
31
+ @apply w-full h-full object-cover;
32
+ }
33
+
34
+ .cms-block-image-two-column :deep(.cms-element-image-slider) {
35
+ @apply w-full;
36
+ }
37
+ </style>
@@ -13,7 +13,7 @@ const rightContent = getSlotContent("right");
13
13
  </script>
14
14
 
15
15
  <template>
16
- <div class="cms-block-product-heading flex justify-between">
16
+ <div class="cms-block-product-heading flex justify-between pt-4">
17
17
  <CmsGenericElement :content="leftContent" />
18
18
  <CmsGenericElement :content="rightContent" />
19
19
  </div>
@@ -14,9 +14,15 @@ const centerContent = getSlotContent("center");
14
14
  </script>
15
15
 
16
16
  <template>
17
- <div class="grid md:grid-cols-3 gap-10 place-items-center">
18
- <CmsGenericElement :content="leftContent" class="w-full" />
19
- <CmsGenericElement :content="centerContent" class="w-full" />
20
- <CmsGenericElement :content="rightContent" class="w-full" />
17
+ <div class="flex flex-col sm:flex-row justify-start items-start gap-6 w-full">
18
+ <div class="w-full sm:flex-1">
19
+ <CmsGenericElement :content="leftContent" class="w-full" />
20
+ </div>
21
+ <div class="w-full sm:flex-1">
22
+ <CmsGenericElement :content="centerContent" class="w-full" />
23
+ </div>
24
+ <div class="w-full sm:flex-1">
25
+ <CmsGenericElement :content="rightContent" class="w-full" />
26
+ </div>
21
27
  </div>
22
28
  </template>
@@ -10,7 +10,9 @@ const props = defineProps<{
10
10
  }>();
11
11
 
12
12
  const { getSlotContent } = useCmsBlock(props.content);
13
- const slotContent = getSlotContent("content") as CmsElementSidebarFilter;
13
+ const slotContent = getSlotContent(
14
+ "content",
15
+ ) as unknown as CmsElementSidebarFilter;
14
16
  </script>
15
17
  <template>
16
18
  <CmsElementSidebarFilter :content="slotContent" />
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import type { CmsBlockTextOnImage } from "@shopware/composables";
3
+ import { getCmsLayoutConfiguration } from "@shopware/helpers";
4
+ import { useCmsBlock } from "#imports";
5
+
6
+ const props = defineProps<{
7
+ content: CmsBlockTextOnImage;
8
+ }>();
9
+
10
+ const { getSlotContent } = useCmsBlock(props.content);
11
+ const { cssClasses, layoutStyles } = getCmsLayoutConfiguration(props.content);
12
+
13
+ const slotContent = getSlotContent("content");
14
+ </script>
15
+
16
+ <template>
17
+ <div
18
+ class="cms-block-text-on-image min-h-[500px] flex items-center justify-center py-20 px-4 bg-cover bg-center bg-no-repeat relative"
19
+ :class="cssClasses"
20
+ :style="layoutStyles as any"
21
+ >
22
+ <div class="relative z-10 text-center max-w-6xl">
23
+ <CmsGenericElement
24
+ v-if="slotContent"
25
+ :content="slotContent"
26
+
27
+ />
28
+ </div>
29
+ </div>
30
+ </template>
@@ -6,15 +6,15 @@ defineProps<{
6
6
  }>();
7
7
  </script>
8
8
  <template>
9
- <div class="container mx-auto flex pt-6 pb-6">
9
+ <div class="mx-auto grid md:grid-cols-3 gap-4 py-6">
10
10
  <CmsGenericElement
11
11
  v-for="(slot, i) in content.slots"
12
12
  :key="slot.id"
13
13
  :content="slot"
14
- class="cms-block-text-teaser-section flex"
14
+ class="cms-block-text-teaser-section"
15
15
  :class="{
16
- 'flex-basis-1/3 flex-col ': i == 0,
17
- 'pl-4 flex-basis-2/3': i == 1,
16
+ 'md:col-span-1': i === 0,
17
+ 'md:col-span-2': i === 1,
18
18
  }"
19
19
  />
20
20
  </div>
@@ -13,16 +13,14 @@ const rightContent = getSlotContent("right");
13
13
  </script>
14
14
 
15
15
  <template>
16
- <article
17
- class="cms-block-text-two-column flex justify-center gap-5 md:gap-20"
18
- >
16
+ <article class="cms-block-text-two-column grid md:grid-cols-2 gap-5 md:gap-20">
19
17
  <CmsGenericElement
20
18
  :content="leftContent"
21
- class="w-1/2 cms-block-text-two-column__text"
19
+ class="cms-block-text-two-column__text"
22
20
  />
23
21
  <CmsGenericElement
24
22
  :content="rightContent"
25
- class="w-1/2 cms-block-text-two-column__text"
23
+ class="cms-block-text-two-column__text"
26
24
  />
27
25
  </article>
28
26
  </template>
@@ -0,0 +1,145 @@
1
+ <script setup lang="ts">
2
+ import type { CmsElementBuyBox } from "@shopware/composables";
3
+ import { useCmsTranslations } from "@shopware/composables";
4
+ import { defu } from "defu";
5
+ import { computed } from "vue";
6
+ import {
7
+ useCmsElementConfig,
8
+ usePrice,
9
+ useProduct,
10
+ useProductPrice,
11
+ useSessionContext,
12
+ } from "#imports";
13
+
14
+ const props = defineProps<{
15
+ content: CmsElementBuyBox;
16
+ }>();
17
+
18
+ type Translations = {
19
+ product: {
20
+ previously: string;
21
+ amount: string;
22
+ price: {
23
+ [key: string]: string;
24
+ };
25
+ to: string;
26
+ from: string;
27
+ content: string;
28
+ pricesIncl: string;
29
+ pricesExcl: string;
30
+ };
31
+ };
32
+
33
+ let translations: Translations = {
34
+ product: {
35
+ previously: "Previously",
36
+ amount: "Amount",
37
+ price: {
38
+ label: "Price",
39
+ to: "To",
40
+ from: "From",
41
+ },
42
+ to: "To",
43
+ from: "From",
44
+ content: "Content",
45
+ pricesIncl: "Prices incl. VAT plus shipping costs",
46
+ pricesExcl: "Prices excl. VAT plus shipping costs",
47
+ },
48
+ };
49
+
50
+ translations = defu(useCmsTranslations(), translations) as Translations;
51
+
52
+ const { getConfigValue } = useCmsElementConfig(props.content);
53
+ const alignment = computed(() => getConfigValue("alignment"));
54
+ const { taxState, currency } = useSessionContext();
55
+ const { product, changeVariant } = useProduct(
56
+ props.content.data.product,
57
+ props.content.data.configuratorSettings || [],
58
+ );
59
+ const { unitPrice, price, tierPrices, isListPrice } = useProductPrice(product);
60
+ const regulationPrice = computed(() => price.value?.regulationPrice?.price);
61
+ const { getFormattedPrice } = usePrice();
62
+ const referencePrice = computed(
63
+ () => product.value?.calculatedPrice?.referencePrice,
64
+ );
65
+ const purchaseUnit = computed(() => product.value?.purchaseUnit);
66
+ const unitName = computed(() => product.value?.unit?.name);
67
+ const productName = computed(() => product.value?.translated?.name || "");
68
+ </script>
69
+ <template>
70
+ <div v-if="product" :class="{
71
+ 'h-full w-full flex flex-col': true,
72
+ 'justify-start': alignment === 'flex-start',
73
+ 'justify-end': alignment === 'flex-end',
74
+ 'justify-center': alignment === 'center',
75
+ }">
76
+ <div class="self-stretch inline-flex flex-col justify-start items-start gap-8 mt-4 min-w-0">
77
+ <div
78
+ class="md:hidden self-stretch text-surface-on-surface text-4xl font-normal font-serif leading-[60px]">
79
+ {{ productName }}</div>
80
+
81
+ <div v-if="tierPrices.length <= 1">
82
+ <SwSharedPrice v-if="isListPrice"
83
+ class="text-1xl text-secondary-900 basis-2/6 justify-start line-through"
84
+ :value="price?.listPrice?.price" />
85
+ <SwSharedPrice v-if="unitPrice"
86
+ class="text-surface-on-surface text-base font-bold leading-normal"
87
+ :class="{
88
+ 'text-red': isListPrice,
89
+ }" :value="unitPrice" />
90
+ <div v-if="regulationPrice" class="text-xs flex text-secondary-500">
91
+ {{ translations.product.previously }}
92
+ <SwSharedPrice class="ml-1" :value="regulationPrice" />
93
+ </div>
94
+ </div>
95
+ <div v-else>
96
+ <table class="border-collapse table-auto w-full text-sm mb-8">
97
+ <thead>
98
+ <tr>
99
+ <th
100
+ class="border-b dark:border-secondary-600 font-medium p-4 pl-8 pt-0 pb-3 text-secondary-600 dark:text-secondary-200 text-left">
101
+ {{ translations.product.amount }}
102
+ </th>
103
+
104
+ <th
105
+ class="border-b dark:border-secondary-600 font-medium p-4 pr-8 pt-0 pb-3 text-secondary-600 dark:text-secondary-200 text-left">
106
+ {{ translations.product.price.label }}
107
+ </th>
108
+ </tr>
109
+ </thead>
110
+ <tbody class="bg-white dark:bg-secondary-800">
111
+ <tr v-for="(tierPrice, index) in tierPrices" :key="tierPrice.label">
112
+ <td
113
+ class="border-b border-secondary-100 dark:border-secondary-700 p-4 pl-8 font-medium text-secondary-500 dark:text-secondary-400">
114
+ <span v-if="index < tierPrices.length - 1">{{
115
+ translations.product.to
116
+ }}</span><span v-else>{{ translations.product.from }}</span>
117
+ {{ tierPrice.quantity }}
118
+ </td>
119
+ <td
120
+ class="border-b border-secondary-100 dark:border-secondary-700 p-4 pr-8 font-medium text-current-500 dark:text-secondary-400">
121
+ {{ getFormattedPrice(tierPrice.unitPrice) }}
122
+ </td>
123
+ </tr>
124
+ </tbody>
125
+ </table>
126
+ </div>
127
+ <div v-if="purchaseUnit && unitName" class="mt-1">
128
+ <span class="font-light"> {{ translations.product.content }}: </span>
129
+ <span class="font-light"> {{ purchaseUnit }} {{ unitName }} </span>
130
+ <span v-if="referencePrice" class="font-light">
131
+ {{ currency?.symbol }} {{ referencePrice?.price }} / /
132
+ {{ referencePrice?.referenceUnit }} {{ referencePrice?.unitName }}
133
+ </span>
134
+ </div>
135
+ <span class="text-brand-primary">
136
+ <template v-if="taxState === 'gross'">
137
+ {{ translations.product.pricesIncl }}
138
+ </template>
139
+ <template v-else> {{ translations.product.pricesExcl }} </template>
140
+ </span>
141
+ <SwVariantConfigurator @change="changeVariant" />
142
+ <SwProductAddToCart :product="product" />
143
+ </div>
144
+ </div>
145
+ </template>
@@ -0,0 +1,53 @@
1
+ <script setup lang="ts">
2
+ import type { Ref } from "vue";
3
+ import { onMounted, ref } from "vue";
4
+ import { useCategory, useNavigation } from "#imports";
5
+ import type { Schemas } from "#shopware";
6
+
7
+ const { category: activeCategory } = useCategory();
8
+ const loading: Ref<boolean> = ref(true);
9
+ const flagAllowSubcategories: boolean = true; // could be passed maybe as a prop in the future
10
+ const categoryNavigation: Ref<Schemas["Category"][]> = ref([]);
11
+
12
+ const currentCategoryId = activeCategory.value?.id ?? "main-navigation";
13
+ const type = flagAllowSubcategories ? currentCategoryId : "main-navigation";
14
+ const { loadNavigationElements } = useNavigation({
15
+ type: type,
16
+ });
17
+ const removeChildrenIfNotActiveCategory = () => {
18
+ const navigation: Schemas["Category"][] = JSON.parse(
19
+ JSON.stringify(categoryNavigation.value),
20
+ );
21
+ return navigation?.map((navigationElement) => {
22
+ navigationElement.children =
23
+ activeCategory.value?.id === navigationElement.id
24
+ ? navigationElement.children
25
+ : [];
26
+ return navigationElement;
27
+ });
28
+ };
29
+
30
+ onMounted(async () => {
31
+ // depth 0 means, we load only first level of categories, depth 1 means we load first and second level of categories ...
32
+ const depth = flagAllowSubcategories ? 2 : 1;
33
+ categoryNavigation.value = await loadNavigationElements({ depth });
34
+ if (!flagAllowSubcategories) {
35
+ categoryNavigation.value = removeChildrenIfNotActiveCategory();
36
+ }
37
+ loading.value = false;
38
+ });
39
+ </script>
40
+ <template>
41
+ <ClientOnly>
42
+ <div v-if="!loading && categoryNavigation && categoryNavigation.length" class="self-stretch inline-flex flex-col justify-start items-start gap-3">
43
+ <SwCategoryNavigation
44
+ :level="0"
45
+ :elements="categoryNavigation"
46
+ :active-category="activeCategory"
47
+ />
48
+ </div>
49
+ <div v-else-if="loading" class="self-stretch flex flex-col justify-start items-start gap-4 animate-pulse">
50
+ <div v-for="i in 3" :key="i" class="w-full h-12 bg-surface-surface-container rounded"></div>
51
+ </div>
52
+ </ClientOnly>
53
+ </template>
@@ -13,7 +13,7 @@ const props = defineProps<{
13
13
 
14
14
  const { getConfigValue } = useCmsElementConfig(props.content);
15
15
  const currentTabIndex = ref<number>(0);
16
- const crossSellContainer = useTemplateRef("crossSellContainer");
16
+ const crossSellContainer = useTemplateRef<HTMLDivElement>("crossSellContainer");
17
17
  const config = computed<SliderElementConfig>(() => ({
18
18
  minHeight: {
19
19
  value: "300px",
@@ -63,9 +63,9 @@ const toggleTab = (index: number) => {
63
63
  <a
64
64
  v-for="(collection, index) of crossSellCollections"
65
65
  :key="index"
66
- class="transition text-lg font-bold text-secondary-700 cursor-pointer"
66
+ class="transition text-lg font-semibold text-surface-on-surface-variant cursor-pointer"
67
67
  :class="{
68
- 'border-b-3 border-primary text-primary': currentTabIndex === index,
68
+ 'border-b-3 border-brand-primary text-brand-primary': currentTabIndex === index,
69
69
  }"
70
70
  @click="toggleTab(index)"
71
71
  >
@@ -3,11 +3,10 @@ import type {
3
3
  CmsElementImage,
4
4
  CmsElementManufacturerLogo,
5
5
  } from "@shopware/composables";
6
- import { buildUrlPrefix } from "@shopware/helpers";
6
+ import { buildUrlPrefix, encodeUrlPath } from "@shopware/helpers";
7
7
  import { useElementSize } from "@vueuse/core";
8
8
  import { computed, defineAsyncComponent, useTemplateRef } from "vue";
9
9
  import { useCmsElementImage, useUrlResolver } from "#imports";
10
- import { ClientOnly } from "../../../../helpers/clientOnly";
11
10
  import { isSpatial } from "../../../../helpers/media/isSpatial";
12
11
 
13
12
  const props = defineProps<{
@@ -27,7 +26,7 @@ const {
27
26
  } = useCmsElementImage(props.content);
28
27
 
29
28
  const DEFAULT_THUMBNAIL_SIZE = 10;
30
- const imageElement = useTemplateRef("imageElement");
29
+ const imageElement = useTemplateRef<HTMLImageElement>("imageElement");
31
30
  const { width, height } = useElementSize(imageElement);
32
31
 
33
32
  function roundUp(num: number) {
@@ -35,11 +34,34 @@ function roundUp(num: number) {
35
34
  }
36
35
 
37
36
  const srcPath = computed(() => {
38
- const biggestParam =
39
- width.value > height.value
40
- ? `width=${roundUp(width.value)}`
41
- : `height=${roundUp(height.value)}`;
42
- return `${imageAttrs.value.src}?${biggestParam}&fit=crop,smart`;
37
+ if (!imageAttrs.value.src) return "";
38
+
39
+ try {
40
+ // Encode the URL first to handle special characters
41
+ const encodedUrl = encodeUrlPath(imageAttrs.value.src);
42
+ const url = new URL(encodedUrl);
43
+
44
+ // Only add size parameters if dimensions are available (after mount)
45
+ // This prevents hydration mismatch
46
+ const w = roundUp(width.value);
47
+ const h = roundUp(height.value);
48
+
49
+ if (w > DEFAULT_THUMBNAIL_SIZE || h > DEFAULT_THUMBNAIL_SIZE) {
50
+ if (width.value > height.value) {
51
+ url.searchParams.set("width", String(w));
52
+ } else {
53
+ url.searchParams.set("height", String(h));
54
+ }
55
+ }
56
+
57
+ // Add fit parameter
58
+ url.searchParams.set("fit", "crop,smart");
59
+
60
+ return url.toString();
61
+ } catch {
62
+ // Fallback if URL parsing fails
63
+ return imageAttrs.value.src;
64
+ }
43
65
  });
44
66
  const imageComputedContainerAttrs = computed(() => {
45
67
  const imageAttrsCopy = Object.assign({}, imageContainerAttrs.value);
@@ -64,7 +86,7 @@ const SwMedia3D = computed(() => {
64
86
  <component
65
87
  :is="imageLink.url ? 'a' : 'div'"
66
88
  v-if="imageAttrs.src"
67
- class="cms-element-image relative h-full w-full"
89
+ class="cms-element-image self-stretch relative"
68
90
  :class="{
69
91
  'flex justify-center items-center': imageGallery,
70
92
  }"
@@ -75,9 +97,10 @@ const SwMedia3D = computed(() => {
75
97
  v-if="isVideoElement"
76
98
  controls
77
99
  :class="{
78
- 'h-full w-full': true,
100
+ 'w-full h-full': true,
79
101
  'absolute inset-0': ['cover', 'stretch'].includes(displayMode),
80
102
  'object-cover': displayMode === 'cover',
103
+ 'object-contain': displayMode !== 'cover',
81
104
  }"
82
105
  >
83
106
  <source :src="imageAttrs.src" :type="mimeType" />
@@ -86,16 +109,17 @@ const SwMedia3D = computed(() => {
86
109
  <ClientOnly v-else-if="isSpatial(props.content.data.media)">
87
110
  <component :is="SwMedia3D" :src="props.content.data.media.url" />
88
111
  </ClientOnly>
89
- <img
112
+ <NuxtImg
90
113
  v-else
91
114
  ref="imageElement"
115
+ preset="productDetail"
92
116
  loading="lazy"
93
117
  :class="{
94
118
  'w-full h-full': !imageGallery,
95
119
  'w-4/5': imageGallery,
96
- 'absolute inset-0': ['cover', 'stretch'].includes(displayMode),
120
+ 'absolute left-0 top-0': ['cover', 'stretch'].includes(displayMode),
97
121
  'object-cover': displayMode === 'cover',
98
- 'object-contain': imageGallery,
122
+ 'object-contain': imageGallery || displayMode !== 'cover',
99
123
  }"
100
124
  :alt="imageAttrs.alt"
101
125
  :src="srcPath"
@@ -103,3 +127,18 @@ const SwMedia3D = computed(() => {
103
127
  />
104
128
  </component>
105
129
  </template>
130
+
131
+ <style scoped>
132
+ .cms-element-image {
133
+ display: block;
134
+ width: 100%;
135
+ height: 100%;
136
+ }
137
+
138
+ /* Ensure anchor tags within the element fill the container */
139
+ .cms-element-image a {
140
+ display: block;
141
+ width: 100%;
142
+ height: 100%;
143
+ }
144
+ </style>
@@ -0,0 +1,158 @@
1
+ <script setup lang="ts">
2
+ import type { CmsElementImageGallery } from "@shopware/composables";
3
+ import { computed, ref } from "vue";
4
+ import { useCmsElementConfig } from "#imports";
5
+ import { isSpatial } from "../../../../helpers/media/isSpatial";
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ content: CmsElementImageGallery;
10
+ slidesToShow?: number;
11
+ slidesToScroll?: number;
12
+ }>(),
13
+ {
14
+ slidesToShow: 5,
15
+ slidesToScroll: 4,
16
+ },
17
+ );
18
+
19
+ const { getConfigValue } = useCmsElementConfig(props.content);
20
+
21
+ const currentIndex = ref(0);
22
+ const mediaGallery = computed(() => props.content.data?.sliderItems ?? []);
23
+
24
+ function goToSlide(index: number) {
25
+ if (index >= 0 && index < mediaGallery.value.length) {
26
+ currentIndex.value = index;
27
+ }
28
+ }
29
+
30
+ function previous() {
31
+ if (currentIndex.value > 0) {
32
+ currentIndex.value--;
33
+ }
34
+ }
35
+
36
+ function next() {
37
+ if (currentIndex.value < mediaGallery.value.length - 1) {
38
+ currentIndex.value++;
39
+ }
40
+ }
41
+
42
+ const currentImage = computed(() => {
43
+ return mediaGallery.value[currentIndex.value]?.media;
44
+ });
45
+
46
+ // Touch event handling for mobile swipe gestures - mobile
47
+ const touchStartX = ref(0);
48
+ const touchEndX = ref(0);
49
+
50
+ function onTouchStart(event: TouchEvent) {
51
+ touchStartX.value = event.touches?.[0]?.clientX || 0;
52
+ }
53
+
54
+ function onTouchMove(event: TouchEvent) {
55
+ touchEndX.value = event?.touches?.[0]?.clientX || 0;
56
+ }
57
+
58
+ function onTouchEnd() {
59
+ const deltaX = touchEndX.value - touchStartX.value;
60
+
61
+ // Define a threshold for swipe detection
62
+ const threshold = 50; // pixels
63
+
64
+ if (Math.abs(deltaX) > threshold) {
65
+ if (deltaX < 0) {
66
+ // Swipe Left
67
+ next();
68
+ } else {
69
+ // Swipe Right
70
+ previous();
71
+ }
72
+ }
73
+
74
+ // Reset values
75
+ touchStartX.value = 0;
76
+ touchEndX.value = 0;
77
+ }
78
+ </script>
79
+
80
+ <template>
81
+ <div class="w-full max-w-full relative inline-flex flex-col justify-center items-center gap-2 mx-auto">
82
+ <div class="w-full">
83
+ <!-- Main Image Display -->
84
+
85
+ <div class="w-full h-[400px] sm:h-[500px] lg:h-[600px] xl:h-[700px] relative overflow-hidden rounded-lg"
86
+ @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
87
+ <Transition name="gallery-fade" mode="out-in">
88
+ <div v-if="currentImage && isSpatial(currentImage)" class="w-full h-full relative">
89
+ <CmsElementImageGallery3dPlaceholder class="w-full h-full absolute inset-0 object-cover" />
90
+ <span class="absolute bottom-4 right-4 text-sm bg-gray-800 rounded px-2 py-1 text-white">
91
+ 3D
92
+ </span>
93
+ </div>
94
+ <NuxtImg v-else-if="currentImage" preset="hero" loading="lazy"
95
+ class="w-full h-full absolute inset-0 object-cover" :src="currentImage.url"
96
+ :key="currentImage.url" :alt="currentImage.alt || 'Product image'" />
97
+ <NuxtImg v-else preset="hero" class="w-full h-full absolute inset-0 object-cover"
98
+ src="https://placehold.co/600x500" alt="Placeholder image" />
99
+ </Transition>
100
+
101
+ </div>
102
+ <!-- Navigation Arrows -->
103
+ <div v-if="mediaGallery.length > 1"
104
+ class="absolute inset-0 flex items-center justify-between px-2 sm:px-4 pointer-events-none">
105
+ <!-- Previous Button -->
106
+ <button
107
+ class="w-10 h-10 bg-brand-tertiary rounded-full hover:bg-brand-tertiary-hover transition-colors disabled:opacity-50 pointer-events-auto shadow-lg"
108
+ :disabled="currentIndex === 0" @click="previous" aria-label="Previous image">
109
+ <div class="flex items-center justify-center w-full h-full">
110
+ <div class="i-carbon-chevron-left w-5 h-5 text-brand-on-tertiary"></div>
111
+ </div>
112
+ </button>
113
+
114
+ <!-- Next Button -->
115
+ <button
116
+ class="w-10 h-10 bg-brand-tertiary rounded-full hover:bg-brand-tertiary-hover transition-colors disabled:opacity-50 pointer-events-auto shadow-lg"
117
+ :disabled="currentIndex === mediaGallery.length - 1" @click="next" aria-label="Next image">
118
+ <div class="flex items-center justify-center w-full h-full">
119
+ <div class="i-carbon-chevron-right w-5 h-5 text-brand-on-tertiary"></div>
120
+ </div>
121
+ </button>
122
+ </div>
123
+
124
+ <!-- Dot Indicators -->
125
+ <div v-if="mediaGallery.length > 1" class="flex justify-center items-center gap-2 mt-2">
126
+ <button v-for="(image, index) in mediaGallery" :key="image.media.url"
127
+ class="relative rounded-full transition-all duration-200 hover:scale-110" :class="{
128
+ 'w-6 h-2 bg-surface-on-surface-variant': index === currentIndex,
129
+ 'w-2 h-2 bg-surface-surface-container-highest': index !== currentIndex
130
+ }" @click="goToSlide(index)" :aria-label="`Go to image ${index + 1}`" />
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </template>
135
+
136
+ <style scoped>
137
+ /* Gallery fade transition */
138
+ .gallery-fade-enter-active,
139
+ .gallery-fade-leave-active {
140
+ transition: all 0.3s ease-in-out;
141
+ }
142
+
143
+ .gallery-fade-enter-from {
144
+ opacity: 0;
145
+ transform: scale(1.05) translateY(10px);
146
+ }
147
+
148
+ .gallery-fade-leave-to {
149
+ opacity: 0;
150
+ transform: scale(0.95) translateY(-10px);
151
+ }
152
+
153
+ .gallery-fade-enter-to,
154
+ .gallery-fade-leave-from {
155
+ opacity: 1;
156
+ transform: scale(1) translateY(0);
157
+ }
158
+ </style>
@@ -11,8 +11,8 @@ const props = defineProps<{
11
11
  const items = computed(() => props.content.data.sliderItems);
12
12
  </script>
13
13
  <template>
14
- <!-- need some with here for small views that the slider can calculate the correct items with -->
15
- <div class="cms-element-image-slider w-[92vw] sm:w-[94vw] md:w-auto">
14
+ <!-- need some width here for small views that the slider can calculate the correct items with -->
15
+ <div class="cms-element-image-slider w-[92vw] sm:w-[94vw] md:w-full">
16
16
  <SwSlider :config="props.content.config">
17
17
  <CmsElementImage
18
18
  v-for="image of items"
@@ -10,5 +10,6 @@ const product = computed(() => props.content.data?.product || {});
10
10
  </script>
11
11
 
12
12
  <template>
13
- <SwProductCard :product="product" />
13
+ <SwProductCard v-if="product?.id" :product="product" />
14
+ <SwProductCardSkeleton v-else />
14
15
  </template>