@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
@@ -0,0 +1,101 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ CmsElementProductSlider,
4
+ SliderElementConfig,
5
+ } from "@shopware/composables";
6
+ import { useElementSize } from "@vueuse/core";
7
+ import { computed, inject, useTemplateRef } from "vue";
8
+ import type { CSSProperties, ComputedRef } from "vue";
9
+ import { useCmsElementConfig } from "#imports";
10
+
11
+ const props = defineProps<{
12
+ content: CmsElementProductSlider;
13
+ }>();
14
+ const { getConfigValue } = useCmsElementConfig(props.content);
15
+
16
+ const productSlider = useTemplateRef<HTMLDivElement>("productSlider");
17
+ const slotCount = inject<number>("cms-block-slot-count", 1);
18
+ const elMinWidth = computed(
19
+ () => +getConfigValue("elMinWidth").replace(/\D+/g, "") || 300,
20
+ );
21
+ const { width } = useElementSize(productSlider);
22
+ const slidesToShow = computed(() => {
23
+ // SSR: useElementSize returns 0, fallback to 1200px estimate divided by slot count
24
+ const containerWidth = width.value || 1200 / slotCount;
25
+ return Math.max(1, Math.floor(containerWidth / elMinWidth.value));
26
+ });
27
+ const products = computed(() => props.content?.data?.products ?? []);
28
+ const config: ComputedRef<SliderElementConfig> = computed(() => ({
29
+ minHeight: {
30
+ value: "450px",
31
+ source: "static",
32
+ },
33
+ verticalAlign: {
34
+ source: "static",
35
+ value: getConfigValue("verticalAlign") || "",
36
+ },
37
+ displayMode: {
38
+ value: "contain",
39
+ source: "static",
40
+ },
41
+ navigationDots: {
42
+ value: getConfigValue("navigation") === true ? "outside" : "",
43
+ source: "static",
44
+ },
45
+ navigationArrows: {
46
+ value: getConfigValue("navigation") === true ? "outside" : "",
47
+ source: "static",
48
+ },
49
+ }));
50
+
51
+ // Responsive SSR breakpoints: scale by slotCount since container is ~1/slotCount of viewport
52
+ const ssrBreakpoints = computed(() => {
53
+ const max = slidesToShow.value;
54
+ const bp: Record<string, number> = {};
55
+ for (let n = 2; n <= max; n++) {
56
+ bp[`(min-width: ${elMinWidth.value * n * slotCount}px)`] = n;
57
+ }
58
+ return bp;
59
+ });
60
+
61
+ const autoplay = computed(() => getConfigValue("rotate"));
62
+ const title = computed(() => getConfigValue("title"));
63
+ const border = computed(() => getConfigValue("border"));
64
+
65
+ const verticalAlignStyle = computed<CSSProperties>(() => ({
66
+ alignContent: getConfigValue("verticalAlign"),
67
+ }));
68
+ const hasVerticalAlignment = computed(
69
+ () => !!verticalAlignStyle.value.alignContent,
70
+ );
71
+ </script>
72
+ <template>
73
+ <div
74
+ :style="hasVerticalAlignment ? verticalAlignStyle : undefined"
75
+ >
76
+ <div ref="productSlider" class="cms-element-product-slider">
77
+ <h3 v-if="title" class="pl-6 pb-6 text-center md:text-left text-surface-on-surface">
78
+ {{ title }}
79
+ </h3>
80
+ <div :class="{ 'py-5 border border-outline-outline-variant': border }">
81
+ <SwSlider
82
+ :config="config"
83
+ gap="1.25rem"
84
+ :slides-to-show="slidesToShow"
85
+ :slides-to-scroll="1"
86
+ :autoplay="autoplay"
87
+ :ssr-breakpoints="ssrBreakpoints"
88
+ >
89
+ <SwProductCard
90
+ v-for="product of products"
91
+ :key="product.id"
92
+ class="h-full"
93
+ :product="product"
94
+ :layout-type="getConfigValue('boxLayout')"
95
+ :display-mode="getConfigValue('displayMode')"
96
+ />
97
+ </SwSlider>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import type { CmsElementSidebarFilter } from "@shopware/composables";
3
+ import { inject } from "vue";
4
+
5
+ defineProps<{
6
+ content: CmsElementSidebarFilter;
7
+ }>();
8
+
9
+ // Inject layout context from parent section
10
+ // If in sidebar section -> use vertical accordion filters
11
+ // Otherwise -> use horizontal dropdown filters
12
+ const sectionLayout = inject<string>("cms-section-layout", "default");
13
+ const isInSidebar = sectionLayout === "sidebar";
14
+ </script>
15
+ <template>
16
+ <div>
17
+ <SwProductListingFilters v-if="isInSidebar" :content="content" />
18
+ <SwProductListingFiltersHorizontal v-else :content="content" />
19
+ </div>
20
+ </template>
@@ -20,10 +20,10 @@ const mappedContent = computed<string>(() => {
20
20
  });
21
21
 
22
22
  const style = computed<CSSProperties>(() => ({
23
- alignItems: getConfigValue("verticalAlign"),
23
+ alignContent: getConfigValue("verticalAlign"),
24
24
  }));
25
25
 
26
- const hasVerticalAlignment = computed(() => !!style.value.alignItems);
26
+ const hasVerticalAlignment = computed(() => !!style.value.alignContent);
27
27
 
28
28
  const CmsTextRender = defineComponent({
29
29
  setup() {
@@ -37,7 +37,7 @@ const CmsTextRender = defineComponent({
37
37
  return (
38
38
  node.type === "tag" &&
39
39
  node.name === "a" &&
40
- !node.attrs?.class?.match(/btn\s?/)
40
+ !node.attrs?.class?.includes("btn")
41
41
  );
42
42
  },
43
43
  renderer(
@@ -61,7 +61,7 @@ const CmsTextRender = defineComponent({
61
61
  return (
62
62
  node.type === "tag" &&
63
63
  node.name === "a" &&
64
- node.attrs?.class?.match(/btn\s?/)
64
+ !!node.attrs?.class?.includes("btn")
65
65
  );
66
66
  },
67
67
  renderer(
@@ -75,8 +75,16 @@ const CmsTextRender = defineComponent({
75
75
  "rounded-md inline-block my-2 py-2 px-4 border border-transparent text-sm font-medium focus:outline-none disabled:opacity-75";
76
76
 
77
77
  _class = node.attrs.class
78
- .replace("btn-secondary", `${btnClass} bg-dark text-white`)
79
- .replace("btn-primary", `${btnClass} bg-primary text-white`);
78
+ .replace(/\bbtn\s+/, "")
79
+ .replace(
80
+ "btn-secondary",
81
+ `${btnClass} bg-brand-secondary text-brand-on-secondary hover:bg-brand-secondary-hover`,
82
+ )
83
+ .replace(
84
+ "btn-primary",
85
+ `${btnClass} bg-brand-primary text-brand-on-primary hover:bg-brand-primary-hover`,
86
+ )
87
+ .trim();
80
88
  }
81
89
 
82
90
  return createElement(
@@ -140,18 +148,15 @@ const CmsTextRender = defineComponent({
140
148
  ? mappedContent.value
141
149
  : "<div class='cms-element-text missing-content-element'></div>";
142
150
 
143
- return () =>
144
- h("div", {}, renderHtml(rawHtml, config, h, context, resolveUrl));
151
+ return () => renderHtml(rawHtml, config, h, context, resolveUrl);
145
152
  },
146
153
  });
147
154
  </script>
148
155
  <template>
149
- <div
150
- :class="{ flex: hasVerticalAlignment, 'flex-row': hasVerticalAlignment }"
151
- :style="style"
152
- >
156
+ <div v-if="hasVerticalAlignment" class="grid h-full" :style="style">
153
157
  <CmsTextRender />
154
158
  </div>
159
+ <CmsTextRender v-else />
155
160
  </template>
156
161
  <style scoped>
157
162
  /** Global CSS styles for text elements */
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ type Translations = {
3
+ listing: {
4
+ perPage: string;
5
+ product: string;
6
+ products: string;
7
+ };
8
+ };
9
+
10
+ defineProps<{
11
+ total: number;
12
+ current: number;
13
+ limit: number;
14
+ translations: Translations;
15
+ }>();
16
+
17
+ const emit = defineEmits<{
18
+ changePage: [number];
19
+ changeLimit: [number];
20
+ }>();
21
+
22
+ const limitModel = defineModel<number>("limit", { required: true });
23
+
24
+ const handlePageChange = (page: number) => {
25
+ emit("changePage", page);
26
+ };
27
+
28
+ const handleLimitChange = (event: Event) => {
29
+ const select = event.target as HTMLSelectElement;
30
+ emit("changeLimit", Number(select.value));
31
+ };
32
+ </script>
33
+
34
+ <template>
35
+ <div v-if="total > 0" class="flex flex-col gap-6 sm:gap-8 mt-6 sm:mt-8">
36
+ <!-- Pagination Controls -->
37
+ <div class="flex justify-center w-full">
38
+ <SwPagination :total="total" :current="current" @change-page="handlePageChange" />
39
+ </div>
40
+
41
+ <!-- Items per page selector -->
42
+ <div class="flex justify-center items-center gap-3 sm:gap-4">
43
+ <label
44
+ for="limit"
45
+ class="text-sm sm:text-base text-surface-on-surface"
46
+ data-testid="listing-pagination-limit-label"
47
+ >
48
+ {{ translations.listing.perPage }}
49
+ </label>
50
+ <div class="relative">
51
+ <select
52
+ id="limit"
53
+ v-model="limitModel"
54
+ name="limitchoices"
55
+ class="appearance-none bg-surface-surface border border-outline-outline hover:border-outline-outline-primary focus:border-outline-outline-primary focus:ring-2 focus:ring-outline-outline-primary focus:ring-opacity-20 px-4 py-2 pr-10 rounded-md text-sm sm:text-base text-surface-on-surface cursor-pointer transition-colors"
56
+ data-testid="listing-pagination-limit-select"
57
+ @change="handleLimitChange"
58
+ >
59
+ <option :value="1">1 {{ translations.listing.product }}</option>
60
+ <option :value="15">15 {{ translations.listing.products }}</option>
61
+ <option :value="30">30 {{ translations.listing.products }}</option>
62
+ <option :value="45">45 {{ translations.listing.products }}</option>
63
+ </select>
64
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
65
+ <SwChevronIcon direction="down" :size="16" />
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </template>
@@ -7,14 +7,14 @@ const props = defineProps<{
7
7
  }>();
8
8
 
9
9
  const { cssClasses, layoutStyles } = getCmsLayoutConfiguration(props.content);
10
+ const { sizingMode: _, ...sectionStyles } = layoutStyles;
10
11
  </script>
11
12
 
12
13
  <template>
13
- <div class="cms-section-default" :class="cssClasses" :styles="layoutStyles">
14
+ <div class="my-4" :class="cssClasses" :style="sectionStyles as any">
14
15
  <CmsGenericBlock
15
16
  v-for="cmsBlock in content.blocks"
16
17
  :key="cmsBlock.id"
17
- class="overflow-auto"
18
18
  :content="cmsBlock"
19
19
  />
20
20
  </div>
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ import { useCmsSection } from "@shopware/composables";
3
+ import type { CmsSectionSidebar } from "@shopware/composables";
4
+ import { computed, provide } from "vue";
5
+
6
+ const props = defineProps<{
7
+ content: CmsSectionSidebar;
8
+ }>();
9
+ const { getPositionContent, section } = useCmsSection(props.content);
10
+
11
+ const sidebarBlocks = getPositionContent("sidebar");
12
+ const mainBlocks = getPositionContent("main");
13
+ const mobileBehavior = computed(() => props.content.mobileBehavior);
14
+ const fullWidth = computed(() => section.sizingMode === "full_width");
15
+
16
+ // Provide layout context for child components
17
+ provide("cms-section-layout", "sidebar");
18
+ </script>
19
+
20
+ <template>
21
+ <div class="self-stretch flex flex-col lg:flex-row items-stretch gap-16" :class="{
22
+ 'px-6': fullWidth,
23
+ }">
24
+ <aside :class="{
25
+ 'w-full lg:w-72 xl:w-80 flex-shrink-0 bg-surface-surface flex flex-col justify-start items-stretch gap-4 lg:sticky lg:top-20 px-4 lg:px-0':
26
+ mobileBehavior !== 'hidden',
27
+ 'hidden lg:block': mobileBehavior === 'hidden',
28
+ }">
29
+ <div v-for="cmsBlock in sidebarBlocks" :key="cmsBlock.id" class="w-full">
30
+ <CmsGenericBlock :content="cmsBlock" />
31
+ </div>
32
+ </aside>
33
+ <div class="flex-1 flex flex-col justify-start items-stretch gap-20">
34
+ <div v-for="cmsBlock in mainBlocks" :key="cmsBlock.id" class="w-full">
35
+ <CmsGenericBlock :content="cmsBlock" />
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { useImagePlaceholder } from "#imports";
3
+
4
+ const placeholderSvg = useImagePlaceholder();
5
+ </script>
6
+
7
+ <template>
8
+ <div role="status" class="p-px flex flex-col justify-start items-start overflow-hidden">
9
+ <!-- Image skeleton -->
10
+ <div class="self-stretch min-h-[350px] relative flex items-center justify-center overflow-hidden aspect-square animate-pulse">
11
+ <img
12
+ :src="placeholderSvg"
13
+ alt=""
14
+ aria-hidden="true"
15
+ class="w-full h-full object-cover"
16
+ />
17
+ </div>
18
+
19
+ <!-- Details skeleton -->
20
+ <div class="w-full pt-4 animate-pulse">
21
+ <div class="h-4 bg-gray-200 rounded-full dark:bg-gray-700 w-3/4 mb-3"></div>
22
+ <div class="h-3 bg-gray-200 rounded-full dark:bg-gray-700 w-1/2 mb-4"></div>
23
+ <div class="h-5 bg-gray-200 rounded-full dark:bg-gray-700 w-20 mb-3"></div>
24
+ <div class="h-10 bg-gray-200 rounded dark:bg-gray-700 w-full"></div>
25
+ </div>
26
+ <span class="sr-only">Loading...</span>
27
+ </div>
28
+ </template>
@@ -0,0 +1,102 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
4
+ export interface SwBaseButtonProps {
5
+ variant?:
6
+ | "primary"
7
+ | "secondary"
8
+ | "success"
9
+ | "warning"
10
+ | "outline"
11
+ | "ghost";
12
+ size?: "small" | "medium" | "large";
13
+ disabled?: boolean;
14
+ loading?: boolean;
15
+ type?: "button" | "submit" | "reset";
16
+ block?: boolean;
17
+ }
18
+
19
+ defineOptions({
20
+ inheritAttrs: false,
21
+ });
22
+
23
+ const {
24
+ variant = "primary",
25
+ size = "medium",
26
+ disabled = false,
27
+ loading = false,
28
+ type = "button",
29
+ block = false,
30
+ } = defineProps<SwBaseButtonProps>();
31
+
32
+ const emit = defineEmits<{
33
+ click: [event: MouseEvent];
34
+ }>();
35
+
36
+ const buttonClasses = computed(() => {
37
+ const classes = [
38
+ "inline-flex justify-center items-center gap-2 rounded font-bold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
39
+ ];
40
+
41
+ const sizeClasses = {
42
+ small: "px-3 py-2 text-sm",
43
+ medium: "px-4 py-3 text-base",
44
+ large: "px-6 py-4 text-lg",
45
+ };
46
+ classes.push(sizeClasses[size]);
47
+
48
+ const variantClasses = {
49
+ primary:
50
+ "bg-brand-primary hover:bg-brand-primary-hover text-brand-on-primary focus:ring-brand-primary",
51
+ secondary:
52
+ "bg-brand-secondary hover:bg-brand-secondary-hover text-brand-on-secondary focus:ring-brand-secondary",
53
+ success:
54
+ "bg-states-success hover:opacity-90 text-white focus:ring-states-success transition-opacity",
55
+ warning:
56
+ "bg-states-warning hover:opacity-90 text-white focus:ring-states-warning transition-opacity",
57
+ outline:
58
+ "border-2 border-brand-primary text-brand-primary hover:bg-brand-primary hover:text-brand-on-primary focus:ring-brand-primary",
59
+ ghost:
60
+ "bg-transparent text-surface-on-surface-variant hover:text-surface-on-surface focus:ring-surface-on-surface",
61
+ };
62
+
63
+ if (disabled || loading) {
64
+ classes.push(
65
+ "bg-surface-surface-disabled text-surface-on-surface cursor-not-allowed opacity-50",
66
+ );
67
+ } else {
68
+ classes.push(variantClasses[variant]);
69
+ }
70
+
71
+ if (block) {
72
+ classes.push("w-full");
73
+ }
74
+
75
+ return classes.join(" ");
76
+ });
77
+
78
+ const handleClick = (event: MouseEvent) => {
79
+ if (!disabled && !loading) {
80
+ emit("click", event);
81
+ }
82
+ };
83
+ </script>
84
+
85
+ <template>
86
+ <button
87
+ :type="type"
88
+ :class="buttonClasses"
89
+ :disabled="disabled || loading"
90
+ @click="handleClick"
91
+ v-bind="$attrs"
92
+ >
93
+ <div
94
+ v-if="loading"
95
+ class="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"
96
+ ></div>
97
+
98
+ <span :class="{ 'opacity-0': loading }">
99
+ <slot />
100
+ </span>
101
+ </button>
102
+ </template>
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ const {
3
+ src,
4
+ size = 24,
5
+ alt = "",
6
+ } = defineProps<{
7
+ src: string;
8
+ size?: number;
9
+ alt?: string;
10
+ }>();
11
+ </script>
12
+
13
+ <template>
14
+ <NuxtImg :src="src" :alt="alt" :width="size" :height="size" />
15
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ const model = defineModel<boolean>({
3
+ required: true,
4
+ });
5
+
6
+ const {
7
+ label,
8
+ description,
9
+ disabled = false,
10
+ } = defineProps<{
11
+ label?: string;
12
+ description?: string;
13
+ disabled?: boolean;
14
+ }>();
15
+ </script>
16
+
17
+ <template>
18
+ <label class="flex items-start gap-2">
19
+ <input
20
+ class="accent-brand-primary w-4 h-4 focus-within:outline-2 focus-within:outline-brand-primary focus-within:outline focus-within:outline-offset-[2px] focus-within"
21
+ type="checkbox"
22
+ v-model="model"
23
+ :disabled
24
+ />
25
+ <div v-if="label || description">
26
+ <p
27
+ v-if="label"
28
+ :class="
29
+ disabled
30
+ ? 'text-surface-on-surface-disabled'
31
+ : 'text-surface-on-surface'
32
+ "
33
+ >
34
+ {{ label }}
35
+ </p>
36
+ <p
37
+ v-if="description"
38
+ class="text-sm"
39
+ :class="
40
+ disabled
41
+ ? 'text-surface-on-surface-disabled'
42
+ : 'text-surface-on-surface-variant'
43
+ "
44
+ >
45
+ {{ description }}
46
+ </p>
47
+ </div>
48
+ </label>
49
+ </template>
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ import CheckmarkFilledSvg from "@cms-assets/icons/check-circle.svg";
3
+ import CheckmarkSvg from "@cms-assets/icons/checkmark.svg";
4
+
5
+ const {
6
+ filled = false,
7
+ size = 24,
8
+ alt = "",
9
+ } = defineProps<{
10
+ filled?: boolean;
11
+ size?: number;
12
+ alt?: string;
13
+ }>();
14
+ </script>
15
+
16
+ <template>
17
+ <NuxtImg
18
+ :src="filled ? CheckmarkFilledSvg : CheckmarkSvg"
19
+ :alt="alt"
20
+ :width="size"
21
+ :height="size"
22
+ />
23
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import ChevronSvg from "@cms-assets/icons/chevron.svg";
3
+ import { computed } from "vue";
4
+
5
+ const {
6
+ direction = "down",
7
+ size = 24,
8
+ alt = "",
9
+ } = defineProps<{
10
+ direction?: "up" | "down" | "left" | "right";
11
+ size?: number;
12
+ alt?: string;
13
+ }>();
14
+
15
+ const rotationClass = computed(() => {
16
+ const rotations = {
17
+ down: "",
18
+ up: "rotate-180",
19
+ left: "rotate-90",
20
+ right: "-rotate-90",
21
+ };
22
+ return rotations[direction];
23
+ });
24
+ </script>
25
+
26
+ <template>
27
+ <NuxtImg
28
+ :src="ChevronSvg"
29
+ :alt="alt"
30
+ :class="['transition-transform', rotationClass]"
31
+ :width="size"
32
+ :height="size"
33
+ />
34
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ import ExclamationCircleSvg from "@cms-assets/icons/exclamation-circle.svg";
3
+
4
+ const { size = 24 } = defineProps<{
5
+ size?: number;
6
+ }>();
7
+ </script>
8
+
9
+ <template>
10
+ <SwBaseIcon :src="ExclamationCircleSvg" :size="size" alt="Error" />
11
+ </template>
@@ -0,0 +1,32 @@
1
+ <script lang="ts" setup>
2
+ const { type = "primary" } = defineProps<{
3
+ type?: "primary" | "secondary" | "tertiary" | "outline" | "ghost";
4
+ }>();
5
+
6
+ const styles = {
7
+ primary:
8
+ "bg-brand-primary hover:focus:bg-brand-primary-hover text-brand-on-primary",
9
+ secondary:
10
+ "bg-brand-secondary hover:focus:bg-brand-secondary-hover text-brand-on-secondary",
11
+ tertiary:
12
+ "bg-brand-tertiary hover:focus:bg-brand-tertiary-hover text-brand-on-tertiary",
13
+ outline:
14
+ "text-brand-primary bg-transparent hover:focus:bg-surface-surface-container outline outline-2 outline-offset-[-2px] outline-brand-primary",
15
+ ghost: "bg-transparent hover:focus:bg-surface-surface-container",
16
+ };
17
+ </script>
18
+
19
+ <template>
20
+ <button
21
+ :class="[
22
+ styles[type],
23
+ {
24
+ 'bg-surface-on-surface-disabled text-surface-surface-disabled':
25
+ $attrs.disabled,
26
+ 'w-10 h-10': type !== 'ghost',
27
+ },
28
+ ]"
29
+ >
30
+ <slot />
31
+ </button>
32
+ </template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ const modelValue = defineModel<string | null>();
3
+
4
+ const { selected } = defineProps<{
5
+ selected: boolean;
6
+ }>();
7
+ </script>
8
+ <template>
9
+ <input
10
+ type="radio"
11
+ class="sr-only"
12
+ v-bind="$attrs"
13
+ v-model="modelValue"
14
+ name="shipping-method"
15
+ />
16
+ <div
17
+ class="w-4 h-4 rounded-full border border-outline-outline border-spacing-1 flex items-center justify-center"
18
+ >
19
+ <div
20
+ :class="{
21
+ 'bg-brand-primary': selected,
22
+ }"
23
+ class="w-2.5 h-2.5 rounded-full"
24
+ ></div>
25
+ </div>
26
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import StarEmptySvg from "@cms-assets/icons/star-empty.svg";
3
+ import StarFilledSvg from "@cms-assets/icons/star-filled.svg";
4
+
5
+ const { filled = true, size = 20 } = defineProps<{
6
+ filled?: boolean;
7
+ size?: number;
8
+ }>();
9
+ </script>
10
+
11
+ <template>
12
+ <NuxtImg
13
+ :src="filled ? StarFilledSvg : StarEmptySvg"
14
+ alt="Star"
15
+ :width="size"
16
+ :height="size"
17
+ />
18
+ </template>