@shopware/cms-base-layer 1.5.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +398 -12
- package/app/app.config.ts +18 -0
- package/app/assets/icons/check-circle.svg +3 -0
- package/app/assets/icons/checkmark.svg +3 -0
- package/app/assets/icons/chevron.svg +3 -0
- package/app/assets/icons/exclamation-circle.svg +3 -0
- package/app/assets/icons/star-empty.svg +3 -0
- package/app/assets/icons/star-filled.svg +3 -0
- package/app/assets/icons/user.svg +1 -0
- package/app/components/SwCategoryNavigation.vue +83 -0
- package/app/components/SwCategoryNavigationLink.vue +128 -0
- package/{components → app/components}/SwContactForm.vue +27 -27
- package/app/components/SwFilterChips.vue +144 -0
- package/app/components/SwFilterDropdown.vue +54 -0
- package/app/components/SwListingProductPrice.vue +89 -0
- package/{components → app/components}/SwMedia3D.vue +4 -2
- package/{components → app/components}/SwNewsletterForm.vue +45 -34
- package/{components → app/components}/SwPagination.vue +3 -5
- package/{components → app/components}/SwProductAddToCart.vue +22 -27
- package/app/components/SwProductCard.vue +169 -0
- package/app/components/SwProductCardDetails.vue +74 -0
- package/app/components/SwProductCardImage.vue +90 -0
- package/app/components/SwProductCardSkeleton.vue +33 -0
- package/app/components/SwProductGallery.vue +43 -0
- package/app/components/SwProductListingFilter.vue +75 -0
- package/app/components/SwProductListingFilters.vue +304 -0
- package/app/components/SwProductListingFiltersHorizontal.vue +306 -0
- package/{components → app/components}/SwProductPrice.vue +3 -3
- package/app/components/SwProductRating.vue +40 -0
- package/{components → app/components}/SwProductReviews.vue +25 -23
- package/app/components/SwProductReviewsForm.vue +292 -0
- package/{components → app/components}/SwProductUnits.vue +10 -15
- package/app/components/SwQuantitySelect.vue +103 -0
- package/{components → app/components}/SwSlider.vue +154 -55
- package/app/components/SwSortDropdown.vue +87 -0
- package/app/components/SwStockInfo.vue +44 -0
- package/{components → app/components}/SwVariantConfigurator.vue +13 -12
- package/app/components/listing-filters/SwFilterPrice.vue +219 -0
- package/app/components/listing-filters/SwFilterProperties.vue +120 -0
- package/app/components/listing-filters/SwFilterRating.vue +99 -0
- package/app/components/listing-filters/SwFilterShippingFree.vue +114 -0
- package/app/components/public/cms/CmsBlockSpatialViewer.vue +94 -0
- package/app/components/public/cms/CmsGenericBlock.md +42 -0
- package/{components → app/components}/public/cms/CmsGenericBlock.vue +15 -1
- package/{components → app/components}/public/cms/CmsPage.md +19 -2
- package/{components → app/components}/public/cms/CmsPage.vue +30 -5
- package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +1 -1
- package/{components → app/components}/public/cms/block/CmsBlockGalleryBuybox.vue +5 -5
- package/{components → app/components}/public/cms/block/CmsBlockImageBubbleRow.vue +5 -5
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +41 -0
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +42 -0
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +37 -0
- package/{components → app/components}/public/cms/block/CmsBlockImageSimpleGrid.vue +11 -5
- package/{components → app/components}/public/cms/block/CmsBlockImageText.vue +7 -3
- package/{components → app/components}/public/cms/block/CmsBlockImageTextBubble.vue +13 -16
- package/{components → app/components}/public/cms/block/CmsBlockImageTextCover.vue +7 -9
- package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +88 -0
- package/app/components/public/cms/block/CmsBlockImageTextRow.vue +53 -0
- package/{components → app/components}/public/cms/block/CmsBlockImageThreeColumn.vue +10 -4
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +37 -0
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +37 -0
- package/{components → app/components}/public/cms/block/CmsBlockProductHeading.vue +1 -1
- package/{components → app/components}/public/cms/block/CmsBlockProductThreeColumn.vue +10 -4
- package/{components → app/components}/public/cms/block/CmsBlockSidebarFilter.vue +3 -1
- package/{components → app/components}/public/cms/block/CmsBlockTextOnImage.vue +8 -5
- package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
- package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
- package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +22 -6
- package/{components → app/components}/public/cms/element/CmsElementImage.vue +58 -21
- package/app/components/public/cms/element/CmsElementImageGallery.vue +225 -0
- package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
- package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +8 -1
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
- package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +31 -95
- package/app/components/public/cms/element/CmsElementProductName.vue +16 -0
- package/app/components/public/cms/element/CmsElementProductSlider.vue +101 -0
- package/app/components/public/cms/element/CmsElementSidebarFilter.vue +20 -0
- package/{components → app/components}/public/cms/element/CmsElementText.vue +17 -12
- package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
- package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +2 -2
- package/app/components/public/cms/section/CmsSectionSidebar.vue +39 -0
- package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
- package/app/components/ui/BaseButton.vue +102 -0
- package/app/components/ui/BaseIcon.vue +15 -0
- package/app/components/ui/Checkbox.vue +49 -0
- package/app/components/ui/CheckmarkIcon.vue +23 -0
- package/app/components/ui/ChevronIcon.vue +34 -0
- package/app/components/ui/ExclamationIcon.vue +11 -0
- package/app/components/ui/IconButton.vue +32 -0
- package/app/components/ui/RadioButton.vue +26 -0
- package/app/components/ui/StarIcon.vue +18 -0
- package/app/components/ui/SwitchButton.vue +100 -0
- package/app/components/ui/UserIcon.vue +11 -0
- package/app/components/ui/WishlistIcon.vue +15 -0
- package/app/composables/useImagePlaceholder.ts +27 -0
- package/app/composables/useLcpImagePreload.test.ts +229 -0
- package/app/composables/useLcpImagePreload.ts +39 -0
- package/{helpers → app/helpers}/clientOnly.ts +5 -0
- package/app/helpers/cms/findFirstCmsImageUrl.ts +86 -0
- package/app/helpers/cms/getImageSizes.test.ts +50 -0
- package/app/helpers/cms/getImageSizes.ts +36 -0
- package/app/helpers/html-to-vue/ast.ts +106 -0
- package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +1 -1
- package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +7 -11
- package/app/helpers/html-to-vue/renderer.ts +116 -0
- package/app/plugins/unocss-runtime.client.ts +23 -0
- package/app/providers/shopware.test.ts +213 -0
- package/app/providers/shopware.ts +107 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +2 -2
- package/index.d.ts +36 -0
- package/nuxt.config.ts +100 -6
- package/package.json +33 -23
- package/uno.config.ts +94 -0
- package/components/SwCategoryNavigation.vue +0 -44
- package/components/SwCategoryNavigationLink.vue +0 -57
- package/components/SwListingProductPrice.vue +0 -89
- package/components/SwProductCard.vue +0 -286
- package/components/SwProductGallery.vue +0 -39
- package/components/SwProductListingFilter.vue +0 -42
- package/components/SwProductListingFilters.vue +0 -292
- package/components/listing-filters/SwFilterPrice.vue +0 -160
- package/components/listing-filters/SwFilterProperties.vue +0 -123
- package/components/listing-filters/SwFilterRating.vue +0 -101
- package/components/listing-filters/SwFilterShippingFree.vue +0 -104
- package/components/public/cms/CmsGenericBlock.md +0 -27
- package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
- package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
- package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
- package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
- package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
- package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
- package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
- package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
- package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
- package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
- package/components/public/cms/element/CmsElementProductName.vue +0 -10
- package/components/public/cms/element/CmsElementProductSlider.vue +0 -80
- package/components/public/cms/element/CmsElementSidebarFilter.vue +0 -12
- package/components/public/cms/section/CmsSectionSidebar.vue +0 -41
- package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
- package/helpers/html-to-vue/ast.ts +0 -72
- package/helpers/html-to-vue/renderer.ts +0 -56
- /package/{components → app/components}/SwSharedPrice.vue +0 -0
- /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
- /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
- /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockHtml.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +0 -0
- /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
- /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
- /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
- /package/{helpers → app/helpers}/media/isSpatial.ts +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsElementImageGallery } from "@shopware/composables";
|
|
3
|
+
import { computed, defineAsyncComponent, ref } from "vue";
|
|
4
|
+
import { useCmsElementConfig, useImagePlaceholder } from "#imports";
|
|
5
|
+
import { isSpatial } from "../../../../helpers/media/isSpatial";
|
|
6
|
+
|
|
7
|
+
// Load SwMedia3D only on client-side to avoid SSR issues with three.js packages
|
|
8
|
+
const SwMedia3DAsync = defineAsyncComponent(
|
|
9
|
+
() => import("../../../SwMedia3D.vue"),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
content: CmsElementImageGallery;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
17
|
+
|
|
18
|
+
const DEFAULT_MIN_HEIGHT = "500px";
|
|
19
|
+
const DEFAULT_NAVIGATION = "inside";
|
|
20
|
+
|
|
21
|
+
const minHeight = computed(
|
|
22
|
+
() => getConfigValue("minHeight") || DEFAULT_MIN_HEIGHT,
|
|
23
|
+
);
|
|
24
|
+
const navigationArrows = computed(
|
|
25
|
+
() => getConfigValue("navigationArrows") || DEFAULT_NAVIGATION,
|
|
26
|
+
);
|
|
27
|
+
const navigationDots = computed(
|
|
28
|
+
() => getConfigValue("navigationDots") || DEFAULT_NAVIGATION,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const currentIndex = ref(0);
|
|
32
|
+
const mediaGallery = computed(() => props.content.data?.sliderItems ?? []);
|
|
33
|
+
const placeholderSvg = useImagePlaceholder();
|
|
34
|
+
|
|
35
|
+
function goToSlide(index: number) {
|
|
36
|
+
if (index >= 0 && index < mediaGallery.value.length) {
|
|
37
|
+
currentIndex.value = index;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function previous() {
|
|
42
|
+
if (currentIndex.value > 0) {
|
|
43
|
+
currentIndex.value--;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function next() {
|
|
48
|
+
if (currentIndex.value < mediaGallery.value.length - 1) {
|
|
49
|
+
currentIndex.value++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const currentImage = computed(() => {
|
|
54
|
+
return mediaGallery.value[currentIndex.value]?.media;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Touch event handling for mobile swipe gestures
|
|
58
|
+
const touchStartX = ref(0);
|
|
59
|
+
const touchEndX = ref(0);
|
|
60
|
+
|
|
61
|
+
function onTouchStart(event: TouchEvent) {
|
|
62
|
+
touchStartX.value = event.touches?.[0]?.clientX || 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function onTouchMove(event: TouchEvent) {
|
|
66
|
+
touchEndX.value = event?.touches?.[0]?.clientX || 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function onTouchEnd() {
|
|
70
|
+
const deltaX = touchEndX.value - touchStartX.value;
|
|
71
|
+
const threshold = 50; // pixels
|
|
72
|
+
|
|
73
|
+
if (Math.abs(deltaX) > threshold) {
|
|
74
|
+
if (deltaX < 0) {
|
|
75
|
+
next();
|
|
76
|
+
} else {
|
|
77
|
+
previous();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
touchStartX.value = 0;
|
|
82
|
+
touchEndX.value = 0;
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<template>
|
|
87
|
+
<div
|
|
88
|
+
class="w-full max-w-full relative inline-flex flex-col justify-center items-center gap-2 mx-auto"
|
|
89
|
+
>
|
|
90
|
+
<div class="w-full">
|
|
91
|
+
<!-- Main Image Display -->
|
|
92
|
+
<div
|
|
93
|
+
class="w-full relative overflow-hidden"
|
|
94
|
+
:style="{ minHeight }"
|
|
95
|
+
@touchstart="onTouchStart"
|
|
96
|
+
@touchmove="onTouchMove"
|
|
97
|
+
@touchend="onTouchEnd"
|
|
98
|
+
>
|
|
99
|
+
<Transition name="gallery-fade" mode="out-in">
|
|
100
|
+
<!-- 3D media -->
|
|
101
|
+
<div
|
|
102
|
+
v-if="currentImage && isSpatial(currentImage)"
|
|
103
|
+
:key="currentImage.url + '-3d'"
|
|
104
|
+
class="w-full h-full relative"
|
|
105
|
+
:style="{ minHeight }"
|
|
106
|
+
>
|
|
107
|
+
<client-only>
|
|
108
|
+
<SwMedia3DAsync :src="currentImage.url" />
|
|
109
|
+
<template #fallback>
|
|
110
|
+
<CmsElementImageGallery3dPlaceholder
|
|
111
|
+
class="w-full h-full absolute inset-0 object-cover"
|
|
112
|
+
/>
|
|
113
|
+
<span
|
|
114
|
+
class="absolute bottom-4 right-4 text-sm bg-gray-800 rounded px-2 py-1 text-white"
|
|
115
|
+
>
|
|
116
|
+
3D
|
|
117
|
+
</span>
|
|
118
|
+
</template>
|
|
119
|
+
</client-only>
|
|
120
|
+
</div>
|
|
121
|
+
<!-- Regular image -->
|
|
122
|
+
<NuxtImg
|
|
123
|
+
v-else-if="currentImage"
|
|
124
|
+
:key="currentImage.url"
|
|
125
|
+
preset="hero"
|
|
126
|
+
loading="lazy"
|
|
127
|
+
class="w-full h-full absolute inset-0 object-cover"
|
|
128
|
+
:placeholder="placeholderSvg"
|
|
129
|
+
:src="currentImage.url"
|
|
130
|
+
:alt="currentImage.alt || 'Product image'"
|
|
131
|
+
/>
|
|
132
|
+
<!-- Placeholder -->
|
|
133
|
+
<img
|
|
134
|
+
v-else
|
|
135
|
+
class="w-full h-full absolute inset-0 object-cover"
|
|
136
|
+
:src="placeholderSvg"
|
|
137
|
+
alt="Placeholder image"
|
|
138
|
+
/>
|
|
139
|
+
</Transition>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Navigation Arrows -->
|
|
143
|
+
<div
|
|
144
|
+
v-if="mediaGallery.length > 1 && navigationArrows !== 'none'"
|
|
145
|
+
class="absolute inset-0 flex items-center justify-between px-2 sm:px-4 pointer-events-none"
|
|
146
|
+
>
|
|
147
|
+
<!-- Previous Button -->
|
|
148
|
+
<button
|
|
149
|
+
:class="[
|
|
150
|
+
'w-10 h-10 rounded-full transition disabled:opacity-50 pointer-events-auto shadow-lg flex items-center justify-center',
|
|
151
|
+
navigationArrows === 'outside'
|
|
152
|
+
? 'bg-brand-tertiary text-surface-on-surface'
|
|
153
|
+
: 'bg-surface-surface/20 hover:bg-surface-surface/50',
|
|
154
|
+
]"
|
|
155
|
+
:disabled="currentIndex === 0"
|
|
156
|
+
aria-label="Previous image"
|
|
157
|
+
@click="previous"
|
|
158
|
+
>
|
|
159
|
+
<SwChevronIcon direction="left" />
|
|
160
|
+
</button>
|
|
161
|
+
|
|
162
|
+
<!-- Next Button -->
|
|
163
|
+
<button
|
|
164
|
+
:class="[
|
|
165
|
+
'w-10 h-10 rounded-full transition disabled:opacity-50 pointer-events-auto shadow-lg flex items-center justify-center',
|
|
166
|
+
navigationArrows === 'outside'
|
|
167
|
+
? 'bg-brand-tertiary text-surface-on-surface'
|
|
168
|
+
: 'bg-surface-surface/20 hover:bg-surface-surface/50',
|
|
169
|
+
]"
|
|
170
|
+
:disabled="currentIndex === mediaGallery.length - 1"
|
|
171
|
+
aria-label="Next image"
|
|
172
|
+
@click="next"
|
|
173
|
+
>
|
|
174
|
+
<SwChevronIcon direction="right" />
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- Dot Indicators -->
|
|
179
|
+
<div
|
|
180
|
+
v-if="mediaGallery.length > 1 && navigationDots !== 'none'"
|
|
181
|
+
:class="[
|
|
182
|
+
'flex justify-center items-center gap-2',
|
|
183
|
+
navigationDots === 'outside' ? 'mt-4' : 'absolute bottom-4 left-1/2 transform -translate-x-1/2',
|
|
184
|
+
]"
|
|
185
|
+
>
|
|
186
|
+
<button
|
|
187
|
+
v-for="(image, index) in mediaGallery"
|
|
188
|
+
:key="image.media?.url"
|
|
189
|
+
class="relative rounded-full transition-all duration-200 hover:scale-110"
|
|
190
|
+
:class="{
|
|
191
|
+
'w-6 h-2 bg-surface-on-surface-variant': index === currentIndex,
|
|
192
|
+
'w-2 h-2 bg-surface-surface-container-highest':
|
|
193
|
+
index !== currentIndex,
|
|
194
|
+
}"
|
|
195
|
+
:aria-label="`Go to image ${index + 1}`"
|
|
196
|
+
@click="goToSlide(index)"
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</template>
|
|
202
|
+
|
|
203
|
+
<style scoped>
|
|
204
|
+
/* Gallery fade transition */
|
|
205
|
+
.gallery-fade-enter-active,
|
|
206
|
+
.gallery-fade-leave-active {
|
|
207
|
+
transition: all 0.3s ease-in-out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.gallery-fade-enter-from {
|
|
211
|
+
opacity: 0;
|
|
212
|
+
transform: scale(1.05) translateY(10px);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.gallery-fade-leave-to {
|
|
216
|
+
opacity: 0;
|
|
217
|
+
transform: scale(0.95) translateY(-10px);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.gallery-fade-enter-to,
|
|
221
|
+
.gallery-fade-leave-from {
|
|
222
|
+
opacity: 1;
|
|
223
|
+
transform: scale(1) translateY(0);
|
|
224
|
+
}
|
|
225
|
+
</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
|
|
15
|
-
<div class="cms-element-image-slider w-[92vw] sm:w-[94vw] md:w-
|
|
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"
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CmsElementProductBox } from "@shopware/composables";
|
|
3
3
|
import { computed } from "vue";
|
|
4
|
+
import { useCmsElementConfig } from "#imports";
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
content: CmsElementProductBox;
|
|
7
8
|
}>();
|
|
8
9
|
|
|
10
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
9
11
|
const product = computed(() => props.content.data?.product || {});
|
|
10
12
|
</script>
|
|
11
13
|
|
|
12
14
|
<template>
|
|
13
|
-
<SwProductCard
|
|
15
|
+
<SwProductCard
|
|
16
|
+
v-if="product?.id"
|
|
17
|
+
:product="product"
|
|
18
|
+
:layout-type="getConfigValue('boxLayout')"
|
|
19
|
+
/>
|
|
20
|
+
<SwProductCardSkeleton v-else />
|
|
14
21
|
</template>
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsElementProductDescriptionReviews } from "@shopware/composables";
|
|
3
|
+
import { useCmsTranslations } from "@shopware/composables";
|
|
4
|
+
import { getTranslatedProperty } from "@shopware/helpers";
|
|
5
|
+
import { defu } from "defu";
|
|
6
|
+
import { type Ref, computed, onMounted, ref } from "vue";
|
|
7
|
+
import xss from "xss";
|
|
8
|
+
import { useProduct, useShopwareContext, useUser } from "#imports";
|
|
9
|
+
import type { Schemas } from "#shopware";
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
content: CmsElementProductDescriptionReviews;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
type Translations = {
|
|
16
|
+
product: {
|
|
17
|
+
description: string;
|
|
18
|
+
reviews: string;
|
|
19
|
+
messages: {
|
|
20
|
+
reviewAdded: string;
|
|
21
|
+
loginToReview: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let translations: Translations = {
|
|
27
|
+
product: {
|
|
28
|
+
description: "Description",
|
|
29
|
+
reviews: "Reviews",
|
|
30
|
+
messages: {
|
|
31
|
+
reviewAdded: "Thank you for submitting your review",
|
|
32
|
+
loginToReview: "Please log in to write a review",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
37
|
+
|
|
38
|
+
const openSections = ref<Set<number>>(new Set([1]));
|
|
39
|
+
const { product } = useProduct(props.content.data?.product);
|
|
40
|
+
|
|
41
|
+
const description = computed(() =>
|
|
42
|
+
xss(getTranslatedProperty(product.value, "description")),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const toggleSection = (sectionNumber: number) => {
|
|
46
|
+
if (openSections.value.has(sectionNumber)) {
|
|
47
|
+
openSections.value.delete(sectionNumber);
|
|
48
|
+
} else {
|
|
49
|
+
openSections.value.add(sectionNumber);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const isSectionOpen = (sectionNumber: number) => {
|
|
54
|
+
return openSections.value.has(sectionNumber);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const reviews: Ref<Schemas["ProductReview"][]> = ref([]);
|
|
58
|
+
const { apiClient } = useShopwareContext();
|
|
59
|
+
const { isLoggedIn } = useUser();
|
|
60
|
+
const reviewAdded = ref(false);
|
|
61
|
+
|
|
62
|
+
const fetchReviews = async () => {
|
|
63
|
+
try {
|
|
64
|
+
const reviewsResponse = await apiClient.invoke(
|
|
65
|
+
"readProductReviews post /product/{productId}/reviews",
|
|
66
|
+
{
|
|
67
|
+
pathParams: { productId: product.value.id },
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
reviews.value = reviewsResponse.data.elements || [];
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("Failed to fetch reviews:", error);
|
|
73
|
+
// Keep existing reviews if fetch fails
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleReviewAdded = () => {
|
|
78
|
+
reviewAdded.value = true;
|
|
79
|
+
fetchReviews();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
onMounted(async () => {
|
|
83
|
+
if (props.content.data?.reviews?.elements?.length) {
|
|
84
|
+
reviews.value = props.content.data.reviews.elements;
|
|
85
|
+
} else {
|
|
86
|
+
await fetchReviews();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<div class="w-full self-stretch inline-flex flex-col justify-start items-start gap-4">
|
|
93
|
+
<div class="self-stretch flex flex-col justify-center items-center">
|
|
94
|
+
<div
|
|
95
|
+
class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-start items-center gap-1 cursor-pointer hover:bg-surface-surface-variant transition-colors"
|
|
96
|
+
@click="toggleSection(1)">
|
|
97
|
+
<div class="flex-1 flex items-center gap-2.5">
|
|
98
|
+
<div class="flex-1 text-surface-on-surface text-base font-bold leading-normal">
|
|
99
|
+
{{ translations.product.description }}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="w-6 h-6 relative">
|
|
103
|
+
<div class="w-2.5 h-1.5 left-[7px] top-[9.50px] absolute">
|
|
104
|
+
<div class="i-carbon-chevron-down transition-transform duration-200"
|
|
105
|
+
:class="{ 'rotate-180': isSectionOpen(1) }"></div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<Transition name="accordion">
|
|
111
|
+
<div v-if="isSectionOpen(1)" class="self-stretch flex flex-col justify-center items-center gap-2.5">
|
|
112
|
+
<div
|
|
113
|
+
class="self-stretch text-surface-on-surface text-base font-normal leading-normal">
|
|
114
|
+
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
115
|
+
<div v-html="description"></div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</Transition>
|
|
119
|
+
<div class="self-stretch flex flex-col justify-center items-center">
|
|
120
|
+
<div
|
|
121
|
+
class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-start items-center gap-1 cursor-pointer hover:bg-surface-surface-variant transition-colors"
|
|
122
|
+
@click="toggleSection(2)">
|
|
123
|
+
<div class="flex-1 flex items-center gap-2.5">
|
|
124
|
+
<div class="flex-1 text-surface-on-surface text-base font-bold leading-normal">
|
|
125
|
+
{{ translations.product.reviews }} ({{ reviews.length }})
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="w-6 h-6 relative">
|
|
129
|
+
<div class="w-2.5 h-1.5 left-[7px] top-[9.50px] absolute">
|
|
130
|
+
<div class="i-carbon-chevron-down transition-transform duration-200"
|
|
131
|
+
:class="{ 'rotate-180': isSectionOpen(2) }"></div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<Transition name="accordion">
|
|
137
|
+
<div v-if="isSectionOpen(2)" class="self-stretch flex flex-col justify-center items-center gap-2.5">
|
|
138
|
+
<div
|
|
139
|
+
class="self-stretch text-surface-on-surface text-base font-normal leading-normal">
|
|
140
|
+
<SwProductReviews v-if="product" :product="product" :reviews="reviews" />
|
|
141
|
+
<ClientOnly>
|
|
142
|
+
<SwProductReviewsForm
|
|
143
|
+
v-if="isLoggedIn && !reviewAdded && product"
|
|
144
|
+
:product-id="product.id"
|
|
145
|
+
@success="handleReviewAdded"
|
|
146
|
+
/>
|
|
147
|
+
<div v-else-if="!isLoggedIn && product" class="mt-4 p-3 bg-surface-surface-container border border-surface-on-surface-variant rounded-md flex gap-2 md:gap-3 items-center">
|
|
148
|
+
<div class="w-5 h-5 text-surface-on-surface-variant flex-shrink-0">
|
|
149
|
+
<SwUserIcon :size="20" />
|
|
150
|
+
</div>
|
|
151
|
+
<span class="text-sm text-surface-on-surface-variant">{{ translations.product.messages.loginToReview }}</span>
|
|
152
|
+
</div>
|
|
153
|
+
</ClientOnly>
|
|
154
|
+
<div v-if="reviewAdded" class="mt-4 p-3 bg-surface-surface-container border border-states-success rounded-md flex gap-2 md:gap-3 items-center">
|
|
155
|
+
<div class="w-5 h-5 text-states-success flex-shrink-0">
|
|
156
|
+
<SwCheckmarkIcon :size="20" :filled="true" alt="Success" />
|
|
157
|
+
</div>
|
|
158
|
+
<span class="text-sm text-states-success">{{ translations.product.messages.reviewAdded }}</span>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</Transition>
|
|
163
|
+
<div class="self-stretch flex flex-col justify-center items-center">
|
|
164
|
+
<div
|
|
165
|
+
class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-start items-center gap-1 cursor-pointer hover:bg-surface-surface-variant transition-colors"
|
|
166
|
+
@click="toggleSection(3)">
|
|
167
|
+
<div class="flex-1 flex items-center gap-2.5">
|
|
168
|
+
<div class="flex-1 text-surface-on-surface text-base font-bold leading-normal">
|
|
169
|
+
Category
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="w-6 h-6 relative">
|
|
173
|
+
<div class="w-2.5 h-1.5 left-[7px] top-[9.50px] absolute">
|
|
174
|
+
<div class="i-carbon-chevron-down transition-transform duration-200"
|
|
175
|
+
:class="{ 'rotate-180': isSectionOpen(3) }"></div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<Transition name="accordion">
|
|
181
|
+
<div v-if="isSectionOpen(3)" class="self-stretch flex flex-col justify-center items-center gap-2.5">
|
|
182
|
+
<div
|
|
183
|
+
class="self-stretch text-surface-on-surface text-base font-normal leading-normal">
|
|
184
|
+
<div v-if="product?.categories">
|
|
185
|
+
<div v-for="category in product.categories" :key="category.id" class="mb-2">
|
|
186
|
+
{{ category.name }}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<div v-else>
|
|
190
|
+
No categories available
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</Transition>
|
|
195
|
+
</div>
|
|
196
|
+
</template>
|
|
197
|
+
<style scoped>
|
|
198
|
+
.accordion-enter-active,
|
|
199
|
+
.accordion-leave-active {
|
|
200
|
+
transition: all 0.3s ease;
|
|
201
|
+
overflow: hidden;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.accordion-enter-from,
|
|
205
|
+
.accordion-leave-to {
|
|
206
|
+
max-height: 0;
|
|
207
|
+
opacity: 0;
|
|
208
|
+
transform: translateY(-10px);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.accordion-enter-to,
|
|
212
|
+
.accordion-leave-from {
|
|
213
|
+
max-height: 500px;
|
|
214
|
+
opacity: 1;
|
|
215
|
+
transform: translateY(0);
|
|
216
|
+
}
|
|
217
|
+
</style>
|
|
@@ -4,17 +4,18 @@ import { useCmsTranslations } from "@shopware/composables";
|
|
|
4
4
|
import { defu } from "defu";
|
|
5
5
|
import { computed, ref, useTemplateRef, watch } from "vue";
|
|
6
6
|
import { useRoute, useRouter } from "vue-router";
|
|
7
|
-
import { useCategoryListing } from "#imports";
|
|
7
|
+
import { useCategoryListing, useCmsElementConfig } from "#imports";
|
|
8
8
|
import type { Schemas, operations } from "#shopware";
|
|
9
9
|
|
|
10
10
|
const props = defineProps<{
|
|
11
11
|
content: CmsElementProductListing;
|
|
12
12
|
}>();
|
|
13
13
|
|
|
14
|
+
const { getConfigValue } = useCmsElementConfig(props.content);
|
|
14
15
|
const defaultLimit = 15;
|
|
15
16
|
const defaultPage = 1;
|
|
16
17
|
const defaultOrder = "name-asc";
|
|
17
|
-
const productListElement = useTemplateRef("productListElement");
|
|
18
|
+
const productListElement = useTemplateRef<HTMLDivElement>("productListElement");
|
|
18
19
|
|
|
19
20
|
type Translations = {
|
|
20
21
|
listing: {
|
|
@@ -87,13 +88,11 @@ const changePage = async (page: number) => {
|
|
|
87
88
|
productListElement.value?.scrollIntoView({ behavior: "smooth" });
|
|
88
89
|
};
|
|
89
90
|
|
|
90
|
-
const changeLimit = async (
|
|
91
|
-
const select = limit.target as HTMLSelectElement;
|
|
92
|
-
|
|
91
|
+
const changeLimit = async (newLimit: number) => {
|
|
93
92
|
await router.push({
|
|
94
93
|
query: {
|
|
95
94
|
...route.query,
|
|
96
|
-
limit:
|
|
95
|
+
limit: newLimit,
|
|
97
96
|
p: defaultPage,
|
|
98
97
|
},
|
|
99
98
|
});
|
|
@@ -151,95 +150,32 @@ compareRouteQueryWithInitialListing();
|
|
|
151
150
|
</script>
|
|
152
151
|
|
|
153
152
|
<template>
|
|
154
|
-
<div class="
|
|
155
|
-
<div class="
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
v-if="loading"
|
|
172
|
-
data-testid="loading"
|
|
173
|
-
class="flex justify-center flex-wrap p-4 md:p-6 lg:p-8"
|
|
174
|
-
>
|
|
175
|
-
<ProductCardSkeleton
|
|
176
|
-
v-for="index in limit"
|
|
177
|
-
:key="index"
|
|
178
|
-
class="w-full mb-8 sm:w-3/7 lg:w-2/7 2xl:w-7/24 mr-0 sm:mr-8 mb-8"
|
|
179
|
-
/>
|
|
180
|
-
</div>
|
|
181
|
-
<div
|
|
182
|
-
class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 lg:gap-8 p-4 md:p-6 lg:p-8"
|
|
183
|
-
>
|
|
184
|
-
<div class="text-center place-self-center">
|
|
185
|
-
<SwPagination
|
|
186
|
-
:total="getTotalPagesCount"
|
|
187
|
-
:current="Number(getCurrentPage)"
|
|
188
|
-
@change-page="changePage"
|
|
189
|
-
/>
|
|
190
|
-
</div>
|
|
191
|
-
<div class="text-center place-self-center mt-2 lg:mt-0">
|
|
192
|
-
<div
|
|
193
|
-
class="inline-block align-top text-center md:text-left"
|
|
194
|
-
data-testid="listing-pagination-limit-box"
|
|
195
|
-
>
|
|
196
|
-
<label
|
|
197
|
-
for="limit"
|
|
198
|
-
class="inline mr-4"
|
|
199
|
-
data-testid="listing-pagination-limit-label"
|
|
200
|
-
>{{ translations.listing.perPage }}</label
|
|
201
|
-
>
|
|
202
|
-
<select
|
|
203
|
-
id="limit"
|
|
204
|
-
v-model="limit"
|
|
205
|
-
name="limitchoices"
|
|
206
|
-
class="inline appearance-none bg-white border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline"
|
|
207
|
-
data-testid="listing-pagination-limit-select"
|
|
208
|
-
@change="changeLimit"
|
|
209
|
-
>
|
|
210
|
-
<option :value="1">1 {{ translations.listing.product }}</option>
|
|
211
|
-
<option :value="15">
|
|
212
|
-
15 {{ translations.listing.products }}
|
|
213
|
-
</option>
|
|
214
|
-
<option :value="30">
|
|
215
|
-
30 {{ translations.listing.products }}
|
|
216
|
-
</option>
|
|
217
|
-
<option :value="45">
|
|
218
|
-
45 {{ translations.listing.products }}
|
|
219
|
-
</option>
|
|
220
|
-
</select>
|
|
221
|
-
<div
|
|
222
|
-
class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
|
|
223
|
-
>
|
|
224
|
-
<svg
|
|
225
|
-
class="fill-current h-4 w-4"
|
|
226
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
227
|
-
viewBox="0 0 20 20"
|
|
228
|
-
>
|
|
229
|
-
<path
|
|
230
|
-
d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
|
|
231
|
-
/>
|
|
232
|
-
</svg>
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
<!-- <div v-else>
|
|
239
|
-
<h2 class="mx-auto text-center">
|
|
240
|
-
{{ translations.listing.noProducts }}
|
|
241
|
-
</h2>
|
|
242
|
-
</div> -->
|
|
153
|
+
<div class="max-w-2xl mx-auto lg:max-w-full">
|
|
154
|
+
<div v-if="!loading && getElements.length < 1" class="text-center text-xl py-16 text-surface-on-surface-variant">
|
|
155
|
+
{{ translations.listing.noProducts }}
|
|
156
|
+
</div>
|
|
157
|
+
<div v-if="!loading" ref="productListElement" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 auto-rows-fr gap-x-4 sm:gap-x-6 lg:gap-x-8 gap-y-8 sm:gap-y-12 lg:gap-y-16">
|
|
158
|
+
<SwProductCard
|
|
159
|
+
v-for="product in getElements"
|
|
160
|
+
:key="product.id"
|
|
161
|
+
:product="product"
|
|
162
|
+
:is-product-listing="isProductListing"
|
|
163
|
+
:layout-type="getConfigValue('boxLayout')"
|
|
164
|
+
class="w-full"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
<div v-if="loading" data-testid="loading" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 auto-rows-fr gap-x-4 sm:gap-x-6 lg:gap-x-8 gap-y-8 sm:gap-y-12 lg:gap-y-16">
|
|
168
|
+
<ProductCardSkeleton v-for="index in limit" :key="index"
|
|
169
|
+
class="w-full" />
|
|
243
170
|
</div>
|
|
171
|
+
<SwProductListingPagination
|
|
172
|
+
v-if="!loading"
|
|
173
|
+
v-model:limit="limit"
|
|
174
|
+
:total="getTotalPagesCount"
|
|
175
|
+
:current="Number(getCurrentPage)"
|
|
176
|
+
:translations="translations"
|
|
177
|
+
@change-page="changePage"
|
|
178
|
+
@change-limit="changeLimit"
|
|
179
|
+
/>
|
|
244
180
|
</div>
|
|
245
181
|
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsElementProductName } from "@shopware/composables";
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
content: CmsElementProductName;
|
|
6
|
+
}>();
|
|
7
|
+
</script>
|
|
8
|
+
<template>
|
|
9
|
+
<!-- there is no css config coming from API for this element so we don't need to merge -->
|
|
10
|
+
<div role="heading" aria-level="1">
|
|
11
|
+
<CmsElementText
|
|
12
|
+
:content="content as any"
|
|
13
|
+
class="self-stretch text-surface-on-surface text-4xl font-normal font-serif leading-[60px]"
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|