@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.
Files changed (109) hide show
  1. package/README.md +93 -135
  2. package/app/app.config.ts +1 -0
  3. package/app/components/SwCategoryNavigation.vue +1 -0
  4. package/app/components/SwCategoryNavigationLink.vue +16 -8
  5. package/app/components/SwContactForm.vue +1 -0
  6. package/app/components/SwFilterChips.vue +1 -0
  7. package/app/components/SwListingProductPrice.vue +16 -4
  8. package/app/components/SwMedia3D.vue +13 -9
  9. package/app/components/SwNewsletterForm.vue +4 -3
  10. package/app/components/SwProductAddToCart.vue +24 -7
  11. package/app/components/SwProductCard.vue +5 -0
  12. package/app/components/SwProductCardDetails.vue +27 -9
  13. package/app/components/SwProductCardImage.vue +52 -35
  14. package/app/components/SwProductCardSkeleton.vue +37 -14
  15. package/app/components/SwProductGallery.vue +1 -0
  16. package/app/components/SwProductListingFilter.vue +5 -1
  17. package/app/components/SwProductListingFilters.vue +23 -8
  18. package/app/components/SwProductListingFiltersHorizontal.vue +6 -3
  19. package/app/components/SwProductPrice.vue +1 -0
  20. package/app/components/SwProductReviews.vue +14 -6
  21. package/app/components/SwProductReviewsForm.vue +15 -3
  22. package/app/components/SwProductUnits.vue +1 -0
  23. package/app/components/SwQuantitySelect.vue +10 -3
  24. package/app/components/SwSharedPrice.vue +1 -0
  25. package/app/components/SwSlider.vue +1 -0
  26. package/app/components/SwStockInfo.vue +12 -6
  27. package/app/components/SwVariantConfigurator.vue +5 -3
  28. package/app/components/listing-filters/SwFilterPrice.vue +79 -30
  29. package/app/components/listing-filters/SwFilterProperties.vue +40 -25
  30. package/app/components/listing-filters/SwFilterRating.vue +21 -9
  31. package/app/components/listing-filters/SwFilterShippingFree.vue +16 -8
  32. package/app/components/public/cms/CmsBlockSpatialViewer.vue +1 -0
  33. package/app/components/public/cms/CmsGenericBlock.vue +11 -4
  34. package/app/components/public/cms/CmsGenericElement.vue +8 -2
  35. package/app/components/public/cms/CmsNoComponent.vue +88 -8
  36. package/app/components/public/cms/CmsPage.md +1 -1
  37. package/app/components/public/cms/CmsPage.vue +5 -6
  38. package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +52 -0
  39. package/app/components/public/cms/block/CmsBlockCategoryNavigation.vue +1 -0
  40. package/app/components/public/cms/block/CmsBlockCenterText.vue +1 -0
  41. package/app/components/public/cms/block/CmsBlockCrossSelling.vue +1 -0
  42. package/app/components/public/cms/block/CmsBlockCustomForm.vue +1 -0
  43. package/app/components/public/cms/block/CmsBlockForm.vue +1 -0
  44. package/app/components/public/cms/block/CmsBlockGalleryBuybox.vue +4 -1
  45. package/app/components/public/cms/block/CmsBlockImage.vue +1 -0
  46. package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +4 -1
  47. package/app/components/public/cms/block/CmsBlockImageCover.vue +1 -0
  48. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +4 -1
  49. package/app/components/public/cms/block/CmsBlockImageGallery.vue +1 -0
  50. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +1 -0
  51. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +4 -1
  52. package/app/components/public/cms/block/CmsBlockImageSimpleGrid.vue +1 -0
  53. package/app/components/public/cms/block/CmsBlockImageSlider.vue +1 -0
  54. package/app/components/public/cms/block/CmsBlockImageText.vue +4 -1
  55. package/app/components/public/cms/block/CmsBlockImageTextBubble.vue +4 -1
  56. package/app/components/public/cms/block/CmsBlockImageTextCover.vue +4 -1
  57. package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +4 -12
  58. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +4 -1
  59. package/app/components/public/cms/block/CmsBlockImageThreeColumn.vue +1 -0
  60. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +4 -1
  61. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +4 -1
  62. package/app/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +1 -0
  63. package/app/components/public/cms/block/CmsBlockProductHeading.vue +1 -0
  64. package/app/components/public/cms/block/CmsBlockProductListing.vue +1 -0
  65. package/app/components/public/cms/block/CmsBlockProductThreeColumn.vue +1 -0
  66. package/app/components/public/cms/block/CmsBlockSidebarFilter.vue +1 -0
  67. package/app/components/public/cms/block/CmsBlockText.vue +1 -0
  68. package/app/components/public/cms/block/CmsBlockTextHero.vue +1 -0
  69. package/app/components/public/cms/block/CmsBlockTextOnImage.vue +2 -4
  70. package/app/components/public/cms/block/CmsBlockTextThreeColumn.vue +1 -0
  71. package/app/components/public/cms/block/CmsBlockTextTwoColumn.vue +4 -1
  72. package/app/components/public/cms/block/CmsBlockVimeoVideo.vue +1 -0
  73. package/app/components/public/cms/block/CmsBlockYoutubeVideo.vue +1 -0
  74. package/app/components/public/cms/element/CmsElementBuyBox.vue +90 -72
  75. package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +14 -3
  76. package/app/components/public/cms/element/CmsElementCrossSelling.vue +3 -1
  77. package/app/components/public/cms/element/CmsElementCustomForm.vue +1 -0
  78. package/app/components/public/cms/element/CmsElementForm.vue +1 -0
  79. package/app/components/public/cms/element/CmsElementHtml.vue +0 -1
  80. package/app/components/public/cms/element/CmsElementImage.vue +15 -34
  81. package/app/components/public/cms/element/CmsElementImageGallery.vue +6 -2
  82. package/app/components/public/cms/element/CmsElementManufacturerLogo.vue +1 -0
  83. package/app/components/public/cms/element/CmsElementProductBox.vue +1 -0
  84. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +77 -29
  85. package/app/components/public/cms/element/CmsElementProductListing.vue +22 -7
  86. package/app/components/public/cms/element/CmsElementProductSlider.vue +6 -4
  87. package/app/components/public/cms/element/CmsElementText.vue +2 -0
  88. package/app/components/public/cms/element/CmsElementVimeoVideo.vue +1 -0
  89. package/app/components/public/cms/element/CmsElementYoutubeVideo.vue +1 -0
  90. package/app/components/public/cms/element/SwProductListingPagination.vue +8 -2
  91. package/app/components/public/cms/section/CmsSectionSidebar.vue +13 -8
  92. package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +16 -5
  93. package/app/components/ui/SwitchButton.vue +53 -14
  94. package/app/components/ui/WishlistIcon.vue +8 -2
  95. package/app/composables/useImagePlaceholder.ts +2 -2
  96. package/app/composables/useLcpImagePreload.test.ts +1 -0
  97. package/app/composables/useLcpImagePreload.ts +12 -6
  98. package/app/composables/useTypedAppConfig.ts +16 -0
  99. package/app/helpers/cms/getImageSizes.test.ts +1 -0
  100. package/app/helpers/html-to-vue/ast.ts +1 -0
  101. package/app/helpers/html-to-vue/getOptionsFromNode.test.ts +1 -0
  102. package/app/helpers/html-to-vue/renderToHtml.ts +1 -0
  103. package/app/helpers/html-to-vue/renderer.ts +1 -0
  104. package/app/providers/shopware.test.ts +1 -0
  105. package/index.d.ts +37 -29
  106. package/nuxt.config.ts +5 -0
  107. package/package.json +22 -25
  108. package/app/plugins/unocss-runtime.client.ts +0 -23
  109. package/uno.config.ts +0 -94
@@ -1,15 +1,20 @@
1
- <script setup lang="ts" generic="
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 class="flex-1 text-surface-on-surface text-base font-bold leading-normal text-left">
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
- class="flex items-center justify-center"
158
- aria-hidden="true"
159
- >
160
- <SwChevronIcon :direction="isFilterVisible ? 'up' : 'down'" :size="24" />
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 v-if="isFilterVisible || displayMode === 'dropdown'" :id="filter.code"
169
- class="self-stretch flex flex-col justify-start items-start gap-2.5">
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 class="self-stretch inline-flex justify-between items-center gap-2">
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
- <input type="number" :placeholder="translations.listing.min" v-model.number="prices.min"
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="emits('select-value', { code: filter.code, value: { min: prices.min, max: prices.max } })"
177
- :min="filter.min" :max="prices.max" />
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
- <input type="number" :placeholder="translations.listing.max" v-model.number="prices.max"
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="emits('select-value', { code: filter.code, value: { min: prices.min, max: prices.max } })"
184
- :min="prices.min" :max="filter.max" />
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 class="relative w-64 h-10 mt-2 mx-auto flex items-center select-none">
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
- </div>
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 class="absolute top-1/2 -translate-y-1/2 h-2 bg-surface-surface-primary rounded-full" :style="{
195
- left: ((prices.min - (filter.min ?? 0)) / ((filter.max ?? 100) - (filter.min ?? 0))) * 100 + '%',
196
- width: ((prices.max - prices.min) / ((filter.max ?? 100) - (filter.min ?? 0))) * 100 + '%',
197
- }"></div>
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)"></div>
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)"></div>
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 class="flex-1 text-surface-on-surface text-base font-bold leading-normal text-left">
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
- class="flex items-center justify-center"
84
- aria-hidden="true"
85
- >
86
- <SwChevronIcon :direction="isFilterVisible ? 'up' : 'down'" :size="24" />
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 v-if="isFilterVisible || displayMode === 'dropdown'" :id="filter.code" class="self-stretch flex flex-col justify-start items-start gap-4">
95
- <fieldset class="self-stretch flex flex-col justify-start items-start gap-4">
96
- <legend class="sr-only">{{ filter.name }}</legend>
97
- <label
98
- v-for="option in filter.options || filter.entities"
99
- :key="`${option.id}-${isChecked(option.id)}`"
100
- class="self-stretch inline-flex justify-start items-start gap-2 cursor-pointer"
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
- <div class="w-4 self-stretch pt-[3px] flex justify-start items-start gap-2.5">
103
- <SwCheckbox
104
- :model-value="isChecked(option.id)"
105
- @update:model-value="() => selectValue(option.id)"
106
- />
107
- </div>
108
- <div class="flex-1 inline-flex flex-col justify-start items-start gap-0.5">
109
- <div class="inline-flex justify-start items-center gap-1">
110
- <div class="flex-1 text-surface-on-surface text-base font-normal leading-normal">
111
- {{ getTranslatedProperty(option, 'name') }}
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
- </div>
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 class="flex-1 text-surface-on-surface text-base font-bold leading-normal text-left">
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
- class="flex items-center justify-center"
74
- aria-hidden="true"
75
- >
76
- <SwChevronIcon :direction="isFilterVisible ? 'up' : 'down'" :size="24" />
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 v-if="isFilterVisible || displayMode === 'dropdown'" class="self-stretch flex flex-col justify-start items-start gap-4">
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="['h-6 w-6 cursor-pointer', displayedScore >= i ? 'i-carbon-star-filled' : 'i-carbon-star']"
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="hoverRating(i); onChangeRating()"
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 class="flex-1 text-surface-on-surface text-base font-bold leading-normal text-left">
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
- class="flex items-center justify-center"
86
- aria-hidden="true"
87
- >
88
- <SwChevronIcon :direction="isFilterVisible ? 'up' : 'down'" :size="24" />
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 v-if="isFilterVisible || displayMode === 'dropdown'" class="self-stretch">
99
+ <div
100
+ v-if="isFilterVisible || displayMode === 'dropdown'"
101
+ class="self-stretch"
102
+ >
97
103
  <div class="pt-6 space-y-4">
98
- <div class="self-stretch inline-flex justify-start items-start gap-2 w-full">
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"
@@ -1,5 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import { computed, defineAsyncComponent } from "vue";
3
+
3
4
  import { useCmsBlock } from "#imports";
4
5
  import type { Schemas } from "#shopware";
5
6
 
@@ -4,16 +4,18 @@ import {
4
4
  getBackgroundImageUrl,
5
5
  getCmsLayoutConfiguration,
6
6
  } from "@shopware/helpers";
7
- import { h, provide } from "vue";
8
- import { useAppConfig } from "#imports";
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 = useAppConfig();
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
- console.error(`Component not resolve: ${componentNameToResolve}`);
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
- console.error(`Component not resolved: ${componentNameToResolve}`);
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
- <span class="sw-text-error">
22
- <b>
23
- {{ elementType }}<i> {{ componentName }} </i>
24
- </b>
25
- is not implemented yet!
26
- </span>
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](../../../../../../README.md#%EF%B8%8F-background-image-optimization) for full details.
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
- import {
10
- createCategoryListingContext,
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 = useAppConfig();
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>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { CmsBlockCategoryNavigation } from "@shopware/composables";
3
+
3
4
  import { useCmsBlock } from "#imports";
4
5
 
5
6
  const props = defineProps<{
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { CmsBlockCenterText } from "@shopware/composables";
3
+
3
4
  import { useCmsBlock } from "#imports";
4
5
 
5
6
  const props = defineProps<{
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { CmsBlockCrossSelling } from "@shopware/composables";
3
+
3
4
  import { useCmsBlock } from "#imports";
4
5
 
5
6
  const props = defineProps<{