@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,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
|
-
|
|
23
|
+
alignContent: getConfigValue("verticalAlign"),
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
|
-
const hasVerticalAlignment = computed(() => !!style.value.
|
|
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?.
|
|
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?.
|
|
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("
|
|
79
|
-
.replace(
|
|
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="
|
|
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>
|