@shopware/cms-base-layer 2.1.0 → 3.0.1
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 +93 -135
- package/app/app.config.ts +1 -0
- package/app/components/SwCategoryNavigation.vue +1 -0
- package/app/components/SwCategoryNavigationLink.vue +16 -8
- package/app/components/SwContactForm.vue +1 -0
- package/app/components/SwFilterChips.vue +1 -0
- package/app/components/SwListingProductPrice.vue +16 -4
- package/app/components/SwMedia3D.vue +13 -9
- package/app/components/SwNewsletterForm.vue +4 -3
- package/app/components/SwProductAddToCart.vue +24 -7
- package/app/components/SwProductCard.vue +5 -0
- package/app/components/SwProductCardDetails.vue +27 -9
- package/app/components/SwProductCardImage.vue +52 -35
- package/app/components/SwProductCardSkeleton.vue +37 -14
- package/app/components/SwProductGallery.vue +1 -0
- package/app/components/SwProductListingFilter.vue +5 -1
- package/app/components/SwProductListingFilters.vue +23 -8
- package/app/components/SwProductListingFiltersHorizontal.vue +6 -3
- package/app/components/SwProductPrice.vue +1 -0
- package/app/components/SwProductReviews.vue +14 -6
- package/app/components/SwProductReviewsForm.vue +15 -3
- package/app/components/SwProductUnits.vue +1 -0
- package/app/components/SwQuantitySelect.vue +10 -3
- package/app/components/SwSharedPrice.vue +1 -0
- package/app/components/SwSlider.vue +1 -0
- package/app/components/SwStockInfo.vue +12 -6
- package/app/components/SwVariantConfigurator.vue +5 -3
- package/app/components/listing-filters/SwFilterPrice.vue +79 -30
- package/app/components/listing-filters/SwFilterProperties.vue +40 -25
- package/app/components/listing-filters/SwFilterRating.vue +21 -9
- package/app/components/listing-filters/SwFilterShippingFree.vue +16 -8
- package/app/components/public/cms/CmsBlockSpatialViewer.vue +1 -0
- package/app/components/public/cms/CmsGenericBlock.vue +11 -4
- package/app/components/public/cms/CmsGenericElement.vue +8 -2
- package/app/components/public/cms/CmsNoComponent.vue +88 -8
- package/app/components/public/cms/CmsPage.md +1 -1
- package/app/components/public/cms/CmsPage.vue +5 -6
- package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +52 -0
- package/app/components/public/cms/block/CmsBlockCategoryNavigation.vue +1 -0
- package/app/components/public/cms/block/CmsBlockCenterText.vue +1 -0
- package/app/components/public/cms/block/CmsBlockCrossSelling.vue +1 -0
- package/app/components/public/cms/block/CmsBlockCustomForm.vue +1 -0
- package/app/components/public/cms/block/CmsBlockForm.vue +1 -0
- package/app/components/public/cms/block/CmsBlockGalleryBuybox.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImage.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageCover.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageGallery.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageSimpleGrid.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageSlider.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageText.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTextBubble.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTextCover.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +4 -12
- package/app/components/public/cms/block/CmsBlockImageTextRow.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageThreeColumn.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +4 -1
- package/app/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +1 -0
- package/app/components/public/cms/block/CmsBlockProductHeading.vue +1 -0
- package/app/components/public/cms/block/CmsBlockProductListing.vue +1 -0
- package/app/components/public/cms/block/CmsBlockProductThreeColumn.vue +1 -0
- package/app/components/public/cms/block/CmsBlockSidebarFilter.vue +1 -0
- package/app/components/public/cms/block/CmsBlockText.vue +1 -0
- package/app/components/public/cms/block/CmsBlockTextHero.vue +1 -0
- package/app/components/public/cms/block/CmsBlockTextOnImage.vue +2 -4
- package/app/components/public/cms/block/CmsBlockTextThreeColumn.vue +1 -0
- package/app/components/public/cms/block/CmsBlockTextTwoColumn.vue +4 -1
- package/app/components/public/cms/block/CmsBlockVimeoVideo.vue +1 -0
- package/app/components/public/cms/block/CmsBlockYoutubeVideo.vue +1 -0
- package/app/components/public/cms/element/CmsElementBuyBox.vue +90 -72
- package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +14 -3
- package/app/components/public/cms/element/CmsElementCrossSelling.vue +3 -1
- package/app/components/public/cms/element/CmsElementCustomForm.vue +1 -0
- package/app/components/public/cms/element/CmsElementForm.vue +1 -0
- package/app/components/public/cms/element/CmsElementHtml.vue +0 -1
- package/app/components/public/cms/element/CmsElementImage.vue +15 -34
- package/app/components/public/cms/element/CmsElementImageGallery.vue +6 -2
- package/app/components/public/cms/element/CmsElementManufacturerLogo.vue +1 -0
- package/app/components/public/cms/element/CmsElementProductBox.vue +1 -0
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +77 -29
- package/app/components/public/cms/element/CmsElementProductListing.vue +22 -7
- package/app/components/public/cms/element/CmsElementProductSlider.vue +6 -4
- package/app/components/public/cms/element/CmsElementText.vue +2 -0
- package/app/components/public/cms/element/CmsElementVimeoVideo.vue +1 -0
- package/app/components/public/cms/element/CmsElementYoutubeVideo.vue +1 -0
- package/app/components/public/cms/element/SwProductListingPagination.vue +8 -2
- package/app/components/public/cms/section/CmsSectionSidebar.vue +13 -8
- package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +16 -5
- package/app/components/ui/SwitchButton.vue +53 -14
- package/app/components/ui/WishlistIcon.vue +8 -2
- package/app/composables/useImagePlaceholder.ts +2 -2
- package/app/composables/useLcpImagePreload.test.ts +1 -0
- package/app/composables/useLcpImagePreload.ts +12 -6
- package/app/composables/useTypedAppConfig.ts +16 -0
- package/app/helpers/cms/getImageSizes.test.ts +1 -0
- package/app/helpers/html-to-vue/ast.ts +1 -0
- package/app/helpers/html-to-vue/getOptionsFromNode.test.ts +1 -0
- package/app/helpers/html-to-vue/renderToHtml.ts +1 -0
- package/app/helpers/html-to-vue/renderer.ts +1 -0
- package/app/providers/shopware.test.ts +1 -0
- package/index.d.ts +37 -29
- package/nuxt.config.ts +5 -0
- package/package.json +22 -25
- package/app/plugins/unocss-runtime.client.ts +0 -23
- package/uno.config.ts +0 -94
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
<script
|
|
1
|
+
<script
|
|
2
|
+
setup
|
|
3
|
+
lang="ts"
|
|
4
|
+
generic="
|
|
2
5
|
ListingFilter extends {
|
|
3
6
|
code: string;
|
|
4
7
|
min?: number;
|
|
5
8
|
max?: number;
|
|
6
9
|
label: string;
|
|
7
10
|
}
|
|
8
|
-
"
|
|
11
|
+
"
|
|
12
|
+
>
|
|
9
13
|
import { useCmsTranslations } from "@shopware/composables";
|
|
10
14
|
import { onClickOutside, useDebounceFn, useEventListener } from "@vueuse/core";
|
|
11
15
|
import { defu } from "defu";
|
|
12
16
|
import { onMounted, reactive, ref, watch } from "vue";
|
|
17
|
+
|
|
13
18
|
import type { Schemas } from "#shopware";
|
|
14
19
|
|
|
15
20
|
const emits =
|
|
@@ -149,15 +154,17 @@ const startDrag = (type: "min" | "max", event: MouseEvent | TouchEvent) => {
|
|
|
149
154
|
@keydown.space.prevent="toggle"
|
|
150
155
|
>
|
|
151
156
|
<div class="flex-1 flex items-center gap-2.5">
|
|
152
|
-
<div
|
|
157
|
+
<div
|
|
158
|
+
class="flex-1 text-surface-on-surface text-base font-bold leading-normal text-left"
|
|
159
|
+
>
|
|
153
160
|
{{ filter.label }}
|
|
154
161
|
</div>
|
|
155
162
|
</div>
|
|
156
|
-
<span
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
<span class="flex items-center justify-center" aria-hidden="true">
|
|
164
|
+
<SwChevronIcon
|
|
165
|
+
:direction="isFilterVisible ? 'up' : 'down'"
|
|
166
|
+
:size="24"
|
|
167
|
+
/>
|
|
161
168
|
</span>
|
|
162
169
|
</div>
|
|
163
170
|
</div>
|
|
@@ -165,52 +172,94 @@ const startDrag = (type: "min" | "max", event: MouseEvent | TouchEvent) => {
|
|
|
165
172
|
|
|
166
173
|
<!-- Filter content -->
|
|
167
174
|
<transition name="filter-collapse">
|
|
168
|
-
<div
|
|
169
|
-
|
|
175
|
+
<div
|
|
176
|
+
v-if="isFilterVisible || displayMode === 'dropdown'"
|
|
177
|
+
:id="filter.code"
|
|
178
|
+
class="self-stretch flex flex-col justify-start items-start gap-2.5"
|
|
179
|
+
>
|
|
170
180
|
<div class="self-stretch flex flex-col justify-start items-start gap-1">
|
|
171
|
-
<div
|
|
181
|
+
<div
|
|
182
|
+
class="self-stretch inline-flex justify-between items-center gap-2"
|
|
183
|
+
>
|
|
172
184
|
<div
|
|
173
|
-
class="w-16 h-10 px-2 py-1 rounded-lg outline outline-1 outline-offset-[-1px] outline-outline-outline-variant inline-flex flex-col justify-center items-start gap-2.5"
|
|
174
|
-
|
|
185
|
+
class="w-16 h-10 px-2 py-1 rounded-lg outline outline-1 outline-offset-[-1px] outline-outline-outline-variant inline-flex flex-col justify-center items-start gap-2.5"
|
|
186
|
+
>
|
|
187
|
+
<input
|
|
188
|
+
type="number"
|
|
189
|
+
:placeholder="translations.listing.min"
|
|
190
|
+
v-model.number="prices.min"
|
|
175
191
|
class="w-full bg-transparent border-none outline-none text-surface-on-surface text-sm font-normal leading-tight"
|
|
176
|
-
@change="
|
|
177
|
-
|
|
192
|
+
@change="
|
|
193
|
+
emits('select-value', {
|
|
194
|
+
code: filter.code,
|
|
195
|
+
value: { min: prices.min, max: prices.max },
|
|
196
|
+
})
|
|
197
|
+
"
|
|
198
|
+
:min="filter.min"
|
|
199
|
+
:max="prices.max"
|
|
200
|
+
/>
|
|
178
201
|
</div>
|
|
179
202
|
<div
|
|
180
|
-
class="w-16 h-10 px-2 py-1 rounded-lg outline outline-1 outline-offset-[-1px] outline-outline-outline-variant inline-flex flex-col justify-center items-start gap-2.5"
|
|
181
|
-
|
|
203
|
+
class="w-16 h-10 px-2 py-1 rounded-lg outline outline-1 outline-offset-[-1px] outline-outline-outline-variant inline-flex flex-col justify-center items-start gap-2.5"
|
|
204
|
+
>
|
|
205
|
+
<input
|
|
206
|
+
type="number"
|
|
207
|
+
:placeholder="translations.listing.max"
|
|
208
|
+
v-model.number="prices.max"
|
|
182
209
|
class="w-full bg-transparent border-none outline-none text-surface-on-surface text-sm font-normal leading-tight"
|
|
183
|
-
@change="
|
|
184
|
-
|
|
210
|
+
@change="
|
|
211
|
+
emits('select-value', {
|
|
212
|
+
code: filter.code,
|
|
213
|
+
value: { min: prices.min, max: prices.max },
|
|
214
|
+
})
|
|
215
|
+
"
|
|
216
|
+
:min="prices.min"
|
|
217
|
+
:max="filter.max"
|
|
218
|
+
/>
|
|
185
219
|
</div>
|
|
186
220
|
</div>
|
|
187
221
|
<!-- Custom slider UI -->
|
|
188
|
-
<div
|
|
222
|
+
<div
|
|
223
|
+
class="relative w-64 h-10 mt-2 mx-auto flex items-center select-none"
|
|
224
|
+
>
|
|
189
225
|
<!-- Track -->
|
|
190
226
|
<div
|
|
191
|
-
class="absolute left-0 top-1/2 -translate-y-1/2 w-full h-2 bg-surface-surface-container-highest rounded-full"
|
|
192
|
-
|
|
227
|
+
class="absolute left-0 top-1/2 -translate-y-1/2 w-full h-2 bg-surface-surface-container-highest rounded-full"
|
|
228
|
+
></div>
|
|
193
229
|
<!-- Active range -->
|
|
194
|
-
<div
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
230
|
+
<div
|
|
231
|
+
class="absolute top-1/2 -translate-y-1/2 h-2 bg-surface-surface-primary rounded-full"
|
|
232
|
+
:style="{
|
|
233
|
+
left:
|
|
234
|
+
((prices.min - (filter.min ?? 0)) /
|
|
235
|
+
((filter.max ?? 100) - (filter.min ?? 0))) *
|
|
236
|
+
100 +
|
|
237
|
+
'%',
|
|
238
|
+
width:
|
|
239
|
+
((prices.max - prices.min) /
|
|
240
|
+
((filter.max ?? 100) - (filter.min ?? 0))) *
|
|
241
|
+
100 +
|
|
242
|
+
'%',
|
|
243
|
+
}"
|
|
244
|
+
></div>
|
|
198
245
|
<!-- Min thumb -->
|
|
199
246
|
<div
|
|
200
247
|
class="absolute top-1/2 -translate-y-1/2 w-5 h-5 bg-brand-primary rounded-full shadow-[2px_2px_10px_0px_rgba(0,0,0,0.15)] cursor-pointer touch-none"
|
|
201
248
|
:style="{
|
|
202
|
-
left: `calc(${((prices.min - (filter.min ?? 0)) / ((filter.max ?? 100) - (filter.min ?? 0))) * 100}% - 10px)
|
|
249
|
+
left: `calc(${((prices.min - (filter.min ?? 0)) / ((filter.max ?? 100) - (filter.min ?? 0))) * 100}% - 10px)`,
|
|
203
250
|
}"
|
|
204
251
|
@mousedown.prevent="startDrag('min', $event)"
|
|
205
|
-
@touchstart.prevent="startDrag('min', $event)"
|
|
252
|
+
@touchstart.prevent="startDrag('min', $event)"
|
|
253
|
+
></div>
|
|
206
254
|
<!-- Max thumb -->
|
|
207
255
|
<div
|
|
208
256
|
class="absolute top-1/2 -translate-y-1/2 w-5 h-5 bg-brand-primary rounded-full shadow-[2px_2px_10px_0px_rgba(0,0,0,0.15)] cursor-pointer touch-none"
|
|
209
257
|
:style="{
|
|
210
|
-
left: `calc(${((prices.max - (filter.min ?? 0)) / ((filter.max ?? 100) - (filter.min ?? 0))) * 100}% - 10px)
|
|
258
|
+
left: `calc(${((prices.max - (filter.min ?? 0)) / ((filter.max ?? 100) - (filter.min ?? 0))) * 100}% - 10px)`,
|
|
211
259
|
}"
|
|
212
260
|
@mousedown.prevent="startDrag('max', $event)"
|
|
213
|
-
@touchstart.prevent="startDrag('max', $event)"
|
|
261
|
+
@touchstart.prevent="startDrag('max', $event)"
|
|
262
|
+
></div>
|
|
214
263
|
</div>
|
|
215
264
|
</div>
|
|
216
265
|
</div>
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
>
|
|
14
14
|
import { getTranslatedProperty } from "@shopware/helpers";
|
|
15
15
|
import { computed, ref } from "vue";
|
|
16
|
+
|
|
16
17
|
import type { Schemas } from "#shopware";
|
|
17
18
|
|
|
18
19
|
const {
|
|
@@ -75,15 +76,17 @@ const selectValue = (id: string) => {
|
|
|
75
76
|
@keydown.space.prevent="toggle"
|
|
76
77
|
>
|
|
77
78
|
<div class="flex-1 flex items-center gap-2.5">
|
|
78
|
-
<div
|
|
79
|
+
<div
|
|
80
|
+
class="flex-1 text-surface-on-surface text-base font-bold leading-normal text-left"
|
|
81
|
+
>
|
|
79
82
|
{{ filter.label }}
|
|
80
83
|
</div>
|
|
81
84
|
</div>
|
|
82
|
-
<span
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
<span class="flex items-center justify-center" aria-hidden="true">
|
|
86
|
+
<SwChevronIcon
|
|
87
|
+
:direction="isFilterVisible ? 'up' : 'down'"
|
|
88
|
+
:size="24"
|
|
89
|
+
/>
|
|
87
90
|
</span>
|
|
88
91
|
</div>
|
|
89
92
|
</div>
|
|
@@ -91,28 +94,40 @@ const selectValue = (id: string) => {
|
|
|
91
94
|
|
|
92
95
|
<!-- Filter content -->
|
|
93
96
|
<transition name="filter-collapse">
|
|
94
|
-
<div
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class="self-stretch
|
|
97
|
+
<div
|
|
98
|
+
v-if="isFilterVisible || displayMode === 'dropdown'"
|
|
99
|
+
:id="filter.code"
|
|
100
|
+
class="self-stretch flex flex-col justify-start items-start gap-4"
|
|
101
|
+
>
|
|
102
|
+
<fieldset
|
|
103
|
+
class="self-stretch flex flex-col justify-start items-start gap-4"
|
|
101
104
|
>
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
<legend class="sr-only">{{ filter.name }}</legend>
|
|
106
|
+
<label
|
|
107
|
+
v-for="option in filter.options || filter.entities"
|
|
108
|
+
:key="`${option.id}-${isChecked(option.id)}`"
|
|
109
|
+
class="self-stretch inline-flex justify-start items-start gap-2 cursor-pointer"
|
|
110
|
+
>
|
|
111
|
+
<div
|
|
112
|
+
class="w-4 self-stretch pt-[3px] flex justify-start items-start gap-2.5"
|
|
113
|
+
>
|
|
114
|
+
<SwCheckbox
|
|
115
|
+
:model-value="isChecked(option.id)"
|
|
116
|
+
@update:model-value="() => selectValue(option.id)"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
<div
|
|
120
|
+
class="flex-1 inline-flex flex-col justify-start items-start gap-0.5"
|
|
121
|
+
>
|
|
122
|
+
<div class="inline-flex justify-start items-center gap-1">
|
|
123
|
+
<div
|
|
124
|
+
class="flex-1 text-surface-on-surface text-base font-normal leading-normal"
|
|
125
|
+
>
|
|
126
|
+
{{ getTranslatedProperty(option, "name") }}
|
|
127
|
+
</div>
|
|
112
128
|
</div>
|
|
113
129
|
</div>
|
|
114
|
-
</
|
|
115
|
-
</label>
|
|
130
|
+
</label>
|
|
116
131
|
</fieldset>
|
|
117
132
|
</div>
|
|
118
133
|
</transition>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"
|
|
10
10
|
>
|
|
11
11
|
import { computed, ref } from "vue";
|
|
12
|
+
|
|
12
13
|
import type { Schemas } from "#shopware";
|
|
13
14
|
|
|
14
15
|
const emits =
|
|
@@ -65,15 +66,17 @@ const toggle = () => {
|
|
|
65
66
|
@keydown.space.prevent="toggle"
|
|
66
67
|
>
|
|
67
68
|
<div class="flex-1 flex items-center gap-2.5">
|
|
68
|
-
<div
|
|
69
|
+
<div
|
|
70
|
+
class="flex-1 text-surface-on-surface text-base font-bold leading-normal text-left"
|
|
71
|
+
>
|
|
69
72
|
{{ filter.label }}
|
|
70
73
|
</div>
|
|
71
74
|
</div>
|
|
72
|
-
<span
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
<span class="flex items-center justify-center" aria-hidden="true">
|
|
76
|
+
<SwChevronIcon
|
|
77
|
+
:direction="isFilterVisible ? 'up' : 'down'"
|
|
78
|
+
:size="24"
|
|
79
|
+
/>
|
|
77
80
|
</span>
|
|
78
81
|
</div>
|
|
79
82
|
</div>
|
|
@@ -81,14 +84,23 @@ const toggle = () => {
|
|
|
81
84
|
|
|
82
85
|
<!-- Filter content -->
|
|
83
86
|
<transition name="filter-collapse">
|
|
84
|
-
<div
|
|
87
|
+
<div
|
|
88
|
+
v-if="isFilterVisible || displayMode === 'dropdown'"
|
|
89
|
+
class="self-stretch flex flex-col justify-start items-start gap-4"
|
|
90
|
+
>
|
|
85
91
|
<div class="flex flex-row items-center gap-2 mt-2">
|
|
86
92
|
<div
|
|
87
93
|
v-for="i in 5"
|
|
88
94
|
:key="i"
|
|
89
|
-
:class="[
|
|
95
|
+
:class="[
|
|
96
|
+
'h-6 w-6 cursor-pointer',
|
|
97
|
+
displayedScore >= i ? 'i-carbon-star-filled' : 'i-carbon-star',
|
|
98
|
+
]"
|
|
90
99
|
@mouseleave="isHoverActive = false"
|
|
91
|
-
@click="
|
|
100
|
+
@click="
|
|
101
|
+
hoverRating(i);
|
|
102
|
+
onChangeRating();
|
|
103
|
+
"
|
|
92
104
|
@mouseover="hoverRating(i)"
|
|
93
105
|
:aria-label="`${i} star${i !== 1 ? 's' : ''}`"
|
|
94
106
|
/>
|
|
@@ -14,6 +14,7 @@ import { useCmsTranslations } from "@shopware/composables";
|
|
|
14
14
|
import { onClickOutside } from "@vueuse/core";
|
|
15
15
|
import { defu } from "defu";
|
|
16
16
|
import { computed, ref } from "vue";
|
|
17
|
+
|
|
17
18
|
import type { Schemas } from "#shopware";
|
|
18
19
|
|
|
19
20
|
const {
|
|
@@ -77,15 +78,17 @@ const handleRadioUpdate = (val: string | null | boolean | undefined) => {
|
|
|
77
78
|
@keydown.space.prevent="toggle"
|
|
78
79
|
>
|
|
79
80
|
<div class="flex-1 flex items-center gap-2.5">
|
|
80
|
-
<div
|
|
81
|
+
<div
|
|
82
|
+
class="flex-1 text-surface-on-surface text-base font-bold leading-normal text-left"
|
|
83
|
+
>
|
|
81
84
|
{{ filter.label }}
|
|
82
85
|
</div>
|
|
83
86
|
</div>
|
|
84
|
-
<span
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
<span class="flex items-center justify-center" aria-hidden="true">
|
|
88
|
+
<SwChevronIcon
|
|
89
|
+
:direction="isFilterVisible ? 'up' : 'down'"
|
|
90
|
+
:size="24"
|
|
91
|
+
/>
|
|
89
92
|
</span>
|
|
90
93
|
</div>
|
|
91
94
|
</div>
|
|
@@ -93,9 +96,14 @@ const handleRadioUpdate = (val: string | null | boolean | undefined) => {
|
|
|
93
96
|
|
|
94
97
|
<!-- Filter content -->
|
|
95
98
|
<transition name="filter-collapse">
|
|
96
|
-
<div
|
|
99
|
+
<div
|
|
100
|
+
v-if="isFilterVisible || displayMode === 'dropdown'"
|
|
101
|
+
class="self-stretch"
|
|
102
|
+
>
|
|
97
103
|
<div class="pt-6 space-y-4">
|
|
98
|
-
<div
|
|
104
|
+
<div
|
|
105
|
+
class="self-stretch inline-flex justify-start items-start gap-2 w-full"
|
|
106
|
+
>
|
|
99
107
|
<div class="flex-1 pt-[3px]">
|
|
100
108
|
<SwSwitchButton
|
|
101
109
|
:model-value="currentFilterData"
|
|
@@ -4,16 +4,18 @@ import {
|
|
|
4
4
|
getBackgroundImageUrl,
|
|
5
5
|
getCmsLayoutConfiguration,
|
|
6
6
|
} from "@shopware/helpers";
|
|
7
|
-
import { h, provide } from "vue";
|
|
8
|
-
|
|
7
|
+
import { h, provide, resolveComponent } from "vue";
|
|
8
|
+
|
|
9
9
|
import type { Schemas } from "#shopware";
|
|
10
|
+
|
|
11
|
+
import { useTypedAppConfig } from "../../../composables/useTypedAppConfig";
|
|
10
12
|
import { getImageSizes } from "../../../helpers/cms/getImageSizes";
|
|
11
13
|
|
|
12
14
|
const props = defineProps<{
|
|
13
15
|
content: Schemas["CmsBlock"];
|
|
14
16
|
}>();
|
|
15
17
|
|
|
16
|
-
const appConfig =
|
|
18
|
+
const appConfig = useTypedAppConfig();
|
|
17
19
|
|
|
18
20
|
const slotCount = props.content.slots?.length || 1;
|
|
19
21
|
provide("cms-block-slot-count", slotCount);
|
|
@@ -67,7 +69,12 @@ const DynamicRender = () => {
|
|
|
67
69
|
}),
|
|
68
70
|
);
|
|
69
71
|
}
|
|
70
|
-
|
|
72
|
+
if (import.meta.dev) {
|
|
73
|
+
console.warn(
|
|
74
|
+
`[CMS] Block type "${componentName}" is not implemented.\n → Create a component named "${componentNameToResolve}.vue" to render it.\n 📖 Docs: https://frontends.shopware.com/getting-started/cms/create-blocks`,
|
|
75
|
+
);
|
|
76
|
+
return h(resolveComponent("CmsNoComponent"), { content: props.content });
|
|
77
|
+
}
|
|
71
78
|
return h("div", {}, "");
|
|
72
79
|
};
|
|
73
80
|
</script>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { resolveCmsComponent } from "@shopware/composables";
|
|
3
3
|
import { getCmsLayoutConfiguration } from "@shopware/helpers";
|
|
4
|
-
import { h } from "vue";
|
|
4
|
+
import { h, resolveComponent } from "vue";
|
|
5
|
+
|
|
5
6
|
import type { Schemas } from "#shopware";
|
|
6
7
|
|
|
7
8
|
const props = defineProps<{
|
|
@@ -28,7 +29,12 @@ const DynamicRender = () => {
|
|
|
28
29
|
class: cssClasses,
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
if (import.meta.dev) {
|
|
33
|
+
console.warn(
|
|
34
|
+
`[CMS] Element type "${componentName}" is not implemented.\n → Create a component named "${componentNameToResolve}.vue" to render it.\n 📖 Docs: https://frontends.shopware.com/getting-started/cms/create-elements`,
|
|
35
|
+
);
|
|
36
|
+
return h(resolveComponent("CmsNoComponent"), { content: props.content });
|
|
37
|
+
}
|
|
32
38
|
return h("div", {}, "");
|
|
33
39
|
};
|
|
34
40
|
</script>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { refAutoReset } from "@vueuse/core";
|
|
3
|
+
import { pascalCase } from "scule";
|
|
2
4
|
import { computed } from "vue";
|
|
5
|
+
|
|
3
6
|
import type { Schemas } from "#shopware";
|
|
4
7
|
|
|
5
8
|
const props = defineProps<{
|
|
6
|
-
content: Schemas["CmsBlock"];
|
|
9
|
+
content: Schemas["CmsSection"] | Schemas["CmsBlock"] | Schemas["CmsSlot"];
|
|
7
10
|
}>();
|
|
8
11
|
|
|
9
12
|
const elementType = computed(() =>
|
|
@@ -15,13 +18,90 @@ const elementType = computed(() =>
|
|
|
15
18
|
);
|
|
16
19
|
|
|
17
20
|
const componentName = computed(() => props.content.type);
|
|
21
|
+
|
|
22
|
+
const expectedComponentName = computed(() =>
|
|
23
|
+
pascalCase(`Cms-${elementType.value}-${componentName.value ?? ""}`),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const docsUrl = computed(() => {
|
|
27
|
+
const params = new URLSearchParams({
|
|
28
|
+
component: expectedComponentName.value,
|
|
29
|
+
type: elementType.value.toLowerCase(),
|
|
30
|
+
});
|
|
31
|
+
return `https://frontends.shopware.com/getting-started/cms/missing-component?${params}`;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const aiPrompt = computed(() => {
|
|
35
|
+
const schemaType =
|
|
36
|
+
props.content.apiAlias === "cms_block"
|
|
37
|
+
? 'Schemas["CmsBlock"]'
|
|
38
|
+
: props.content.apiAlias === "cms_section"
|
|
39
|
+
? 'Schemas["CmsSection"]'
|
|
40
|
+
: 'Schemas["CmsSlot"]';
|
|
41
|
+
|
|
42
|
+
const contentJson = JSON.stringify(props.content, null, 2);
|
|
43
|
+
const type = elementType.value.toLowerCase();
|
|
44
|
+
const name = componentName.value;
|
|
45
|
+
const compName = expectedComponentName.value;
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
`Create a Vue 3 component \`${compName}.vue\` for a Shopware Frontends headless storefront.`,
|
|
49
|
+
"",
|
|
50
|
+
`This component renders the CMS ${type} type: "${name}" (apiAlias: "${props.content.apiAlias}").`,
|
|
51
|
+
"",
|
|
52
|
+
"Requirements:",
|
|
53
|
+
`- Accept a \`content: ${schemaType}\` prop`,
|
|
54
|
+
`- Render the data for CMS ${type} type "${name}"`,
|
|
55
|
+
`- Follow patterns from existing Cms${elementType.value} components in packages/cms-base-layer/app/components/public/cms/${type}/`,
|
|
56
|
+
"- Use composables from @shopware/composables where applicable",
|
|
57
|
+
"",
|
|
58
|
+
`The full content prop for this ${type} is:`,
|
|
59
|
+
contentJson,
|
|
60
|
+
"",
|
|
61
|
+
`Reference: ${docsUrl.value}`,
|
|
62
|
+
].join("\n");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const copied = refAutoReset(false, 2000);
|
|
66
|
+
|
|
67
|
+
async function copyPrompt() {
|
|
68
|
+
try {
|
|
69
|
+
await navigator.clipboard.writeText(aiPrompt.value);
|
|
70
|
+
copied.value = true;
|
|
71
|
+
} catch {
|
|
72
|
+
// Fallback: clipboard API unavailable (non-secure origin, unfocused document)
|
|
73
|
+
console.warn("[CMS] Could not copy to clipboard. Prompt logged below:");
|
|
74
|
+
console.info(aiPrompt.value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
18
77
|
</script>
|
|
19
|
-
|
|
78
|
+
|
|
20
79
|
<template>
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
80
|
+
<div
|
|
81
|
+
class="sw-cms-no-component box-border min-h-[40px] rounded border-2 border-dashed border-brand-primary bg-brand-secondary font-mono text-[11px] text-brand-on-secondary"
|
|
82
|
+
>
|
|
83
|
+
<div class="flex flex-wrap items-center gap-1.5 px-2 py-1.5">
|
|
84
|
+
<span class="text-brand-primary">⚠ missing implementation:</span>
|
|
85
|
+
<span class="font-semibold">{{ expectedComponentName }}</span>
|
|
86
|
+
|
|
87
|
+
<a
|
|
88
|
+
:href="docsUrl"
|
|
89
|
+
target="_blank"
|
|
90
|
+
rel="noopener noreferrer"
|
|
91
|
+
title="View CMS documentation"
|
|
92
|
+
class="whitespace-nowrap rounded border border-outline-outline-primary bg-surface-surface px-[5px] py-px text-[10px] leading-none text-brand-primary no-underline hover:bg-brand-secondary-hover"
|
|
93
|
+
>
|
|
94
|
+
docs ↗
|
|
95
|
+
</a>
|
|
96
|
+
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
title="Copy AI prompt to clipboard"
|
|
100
|
+
class="cursor-pointer whitespace-nowrap rounded border border-outline-outline-primary bg-surface-surface px-[5px] py-px font-mono text-[10px] leading-none text-brand-primary hover:bg-brand-secondary-hover"
|
|
101
|
+
@click.stop="copyPrompt"
|
|
102
|
+
>
|
|
103
|
+
{{ copied ? "copied ✓" : "copy AI prompt" }}
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
27
107
|
</template>
|
|
@@ -15,7 +15,7 @@ export default defineAppConfig({
|
|
|
15
15
|
});
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
See the [cms-base-layer README](
|
|
18
|
+
See the [cms-base-layer README](https://github.com/shopware/frontends/tree/main/packages/cms-base-layer#%EF%B8%8F-background-image-optimization) for full details.
|
|
19
19
|
|
|
20
20
|
### Example usage
|
|
21
21
|
|
|
@@ -6,20 +6,19 @@ import {
|
|
|
6
6
|
} from "@shopware/helpers";
|
|
7
7
|
import { pascalCase } from "scule";
|
|
8
8
|
import { computed, h, resolveComponent, watchEffect } from "vue";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
useAppConfig,
|
|
12
|
-
useNavigationContext,
|
|
13
|
-
} from "#imports";
|
|
9
|
+
|
|
10
|
+
import { createCategoryListingContext, useNavigationContext } from "#imports";
|
|
14
11
|
import type { Schemas } from "#shopware";
|
|
12
|
+
|
|
15
13
|
import { useLcpImagePreload } from "../../../composables/useLcpImagePreload";
|
|
14
|
+
import { useTypedAppConfig } from "../../../composables/useTypedAppConfig";
|
|
16
15
|
|
|
17
16
|
const props = defineProps<{
|
|
18
17
|
content: Schemas["CmsPage"];
|
|
19
18
|
}>();
|
|
20
19
|
|
|
21
20
|
const { routeName } = useNavigationContext();
|
|
22
|
-
const appConfig =
|
|
21
|
+
const appConfig = useTypedAppConfig();
|
|
23
22
|
|
|
24
23
|
// Function to initialize or update listing context
|
|
25
24
|
function updateListingContext(content: Schemas["CmsPage"]) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{
|
|
3
|
+
navigationId: string;
|
|
4
|
+
}>();
|
|
5
|
+
|
|
6
|
+
const { apiClient } = useShopwareContext();
|
|
7
|
+
|
|
8
|
+
const { data: registrationResponse } = await useAsyncData(
|
|
9
|
+
`cmsNavigation${props.navigationId}`,
|
|
10
|
+
async () => {
|
|
11
|
+
const response = await apiClient.invoke(
|
|
12
|
+
"getCustomerGroupRegistrationInfo get /customer-group-registration/config/{customerGroupId}",
|
|
13
|
+
{
|
|
14
|
+
pathParams: { customerGroupId: props.navigationId },
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
return response.data || {};
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
useSeoMeta({
|
|
22
|
+
description: () =>
|
|
23
|
+
registrationResponse.value?.translated?.registrationSeoMetaDescription ||
|
|
24
|
+
undefined,
|
|
25
|
+
});
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div class="container mx-auto bg-surface-surface flex flex-col mt-10">
|
|
30
|
+
<h1
|
|
31
|
+
class="mb-4 text-2xl font-extrabold leading-none tracking-tight text-surface-on-surface md:text-3xl lg:text-5xl text-center"
|
|
32
|
+
>
|
|
33
|
+
{{ registrationResponse?.translated.registrationTitle }}
|
|
34
|
+
</h1>
|
|
35
|
+
<div
|
|
36
|
+
v-if="registrationResponse?.registrationActive"
|
|
37
|
+
class="text-lg font-normal text-surface-on-surface-variant lg:text-xl"
|
|
38
|
+
>
|
|
39
|
+
<div
|
|
40
|
+
v-if="registrationResponse?.translated.registrationIntroduction"
|
|
41
|
+
class="px-6 sm:px-4 mb-6"
|
|
42
|
+
v-html="registrationResponse.translated.registrationIntroduction"
|
|
43
|
+
/>
|
|
44
|
+
<AccountRegistrationForm
|
|
45
|
+
:customer-group-id="registrationResponse?.id"
|
|
46
|
+
:company-only="
|
|
47
|
+
registrationResponse?.translated?.registrationOnlyCompanyRegistration
|
|
48
|
+
"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|