@shopware/cms-base-layer 1.5.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +398 -12
  2. package/app/app.config.ts +18 -0
  3. package/app/assets/icons/check-circle.svg +3 -0
  4. package/app/assets/icons/checkmark.svg +3 -0
  5. package/app/assets/icons/chevron.svg +3 -0
  6. package/app/assets/icons/exclamation-circle.svg +3 -0
  7. package/app/assets/icons/star-empty.svg +3 -0
  8. package/app/assets/icons/star-filled.svg +3 -0
  9. package/app/assets/icons/user.svg +1 -0
  10. package/app/components/SwCategoryNavigation.vue +83 -0
  11. package/app/components/SwCategoryNavigationLink.vue +128 -0
  12. package/{components → app/components}/SwContactForm.vue +27 -27
  13. package/app/components/SwFilterChips.vue +144 -0
  14. package/app/components/SwFilterDropdown.vue +54 -0
  15. package/app/components/SwListingProductPrice.vue +89 -0
  16. package/{components → app/components}/SwMedia3D.vue +4 -2
  17. package/{components → app/components}/SwNewsletterForm.vue +45 -34
  18. package/{components → app/components}/SwPagination.vue +3 -5
  19. package/{components → app/components}/SwProductAddToCart.vue +22 -27
  20. package/app/components/SwProductCard.vue +169 -0
  21. package/app/components/SwProductCardDetails.vue +74 -0
  22. package/app/components/SwProductCardImage.vue +90 -0
  23. package/app/components/SwProductCardSkeleton.vue +33 -0
  24. package/app/components/SwProductGallery.vue +43 -0
  25. package/app/components/SwProductListingFilter.vue +75 -0
  26. package/app/components/SwProductListingFilters.vue +304 -0
  27. package/app/components/SwProductListingFiltersHorizontal.vue +306 -0
  28. package/{components → app/components}/SwProductPrice.vue +3 -3
  29. package/app/components/SwProductRating.vue +40 -0
  30. package/{components → app/components}/SwProductReviews.vue +25 -23
  31. package/app/components/SwProductReviewsForm.vue +292 -0
  32. package/{components → app/components}/SwProductUnits.vue +10 -15
  33. package/app/components/SwQuantitySelect.vue +103 -0
  34. package/{components → app/components}/SwSlider.vue +154 -55
  35. package/app/components/SwSortDropdown.vue +87 -0
  36. package/app/components/SwStockInfo.vue +44 -0
  37. package/{components → app/components}/SwVariantConfigurator.vue +13 -12
  38. package/app/components/listing-filters/SwFilterPrice.vue +219 -0
  39. package/app/components/listing-filters/SwFilterProperties.vue +120 -0
  40. package/app/components/listing-filters/SwFilterRating.vue +99 -0
  41. package/app/components/listing-filters/SwFilterShippingFree.vue +114 -0
  42. package/app/components/public/cms/CmsBlockSpatialViewer.vue +94 -0
  43. package/app/components/public/cms/CmsGenericBlock.md +42 -0
  44. package/{components → app/components}/public/cms/CmsGenericBlock.vue +15 -1
  45. package/{components → app/components}/public/cms/CmsPage.md +19 -2
  46. package/{components → app/components}/public/cms/CmsPage.vue +30 -5
  47. package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +1 -1
  48. package/{components → app/components}/public/cms/block/CmsBlockGalleryBuybox.vue +5 -5
  49. package/{components → app/components}/public/cms/block/CmsBlockImageBubbleRow.vue +5 -5
  50. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +41 -0
  51. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +42 -0
  52. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +37 -0
  53. package/{components → app/components}/public/cms/block/CmsBlockImageSimpleGrid.vue +11 -5
  54. package/{components → app/components}/public/cms/block/CmsBlockImageText.vue +7 -3
  55. package/{components → app/components}/public/cms/block/CmsBlockImageTextBubble.vue +13 -16
  56. package/{components → app/components}/public/cms/block/CmsBlockImageTextCover.vue +7 -9
  57. package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +88 -0
  58. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +53 -0
  59. package/{components → app/components}/public/cms/block/CmsBlockImageThreeColumn.vue +10 -4
  60. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +37 -0
  61. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +37 -0
  62. package/{components → app/components}/public/cms/block/CmsBlockProductHeading.vue +1 -1
  63. package/{components → app/components}/public/cms/block/CmsBlockProductThreeColumn.vue +10 -4
  64. package/{components → app/components}/public/cms/block/CmsBlockSidebarFilter.vue +3 -1
  65. package/{components → app/components}/public/cms/block/CmsBlockTextOnImage.vue +8 -5
  66. package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
  67. package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
  68. package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +22 -6
  69. package/{components → app/components}/public/cms/element/CmsElementImage.vue +58 -21
  70. package/app/components/public/cms/element/CmsElementImageGallery.vue +225 -0
  71. package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
  72. package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +8 -1
  73. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
  74. package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +31 -95
  75. package/app/components/public/cms/element/CmsElementProductName.vue +16 -0
  76. package/app/components/public/cms/element/CmsElementProductSlider.vue +101 -0
  77. package/app/components/public/cms/element/CmsElementSidebarFilter.vue +20 -0
  78. package/{components → app/components}/public/cms/element/CmsElementText.vue +17 -12
  79. package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
  80. package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +2 -2
  81. package/app/components/public/cms/section/CmsSectionSidebar.vue +39 -0
  82. package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
  83. package/app/components/ui/BaseButton.vue +102 -0
  84. package/app/components/ui/BaseIcon.vue +15 -0
  85. package/app/components/ui/Checkbox.vue +49 -0
  86. package/app/components/ui/CheckmarkIcon.vue +23 -0
  87. package/app/components/ui/ChevronIcon.vue +34 -0
  88. package/app/components/ui/ExclamationIcon.vue +11 -0
  89. package/app/components/ui/IconButton.vue +32 -0
  90. package/app/components/ui/RadioButton.vue +26 -0
  91. package/app/components/ui/StarIcon.vue +18 -0
  92. package/app/components/ui/SwitchButton.vue +100 -0
  93. package/app/components/ui/UserIcon.vue +11 -0
  94. package/app/components/ui/WishlistIcon.vue +15 -0
  95. package/app/composables/useImagePlaceholder.ts +27 -0
  96. package/app/composables/useLcpImagePreload.test.ts +229 -0
  97. package/app/composables/useLcpImagePreload.ts +39 -0
  98. package/{helpers → app/helpers}/clientOnly.ts +5 -0
  99. package/app/helpers/cms/findFirstCmsImageUrl.ts +86 -0
  100. package/app/helpers/cms/getImageSizes.test.ts +50 -0
  101. package/app/helpers/cms/getImageSizes.ts +36 -0
  102. package/app/helpers/html-to-vue/ast.ts +106 -0
  103. package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +1 -1
  104. package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +7 -11
  105. package/app/helpers/html-to-vue/renderer.ts +116 -0
  106. package/app/plugins/unocss-runtime.client.ts +23 -0
  107. package/app/providers/shopware.test.ts +213 -0
  108. package/app/providers/shopware.ts +107 -0
  109. package/dist/index.d.mts +3 -3
  110. package/dist/index.d.ts +3 -3
  111. package/dist/index.mjs +2 -2
  112. package/index.d.ts +36 -0
  113. package/nuxt.config.ts +100 -6
  114. package/package.json +33 -23
  115. package/uno.config.ts +94 -0
  116. package/components/SwCategoryNavigation.vue +0 -44
  117. package/components/SwCategoryNavigationLink.vue +0 -57
  118. package/components/SwListingProductPrice.vue +0 -89
  119. package/components/SwProductCard.vue +0 -286
  120. package/components/SwProductGallery.vue +0 -39
  121. package/components/SwProductListingFilter.vue +0 -42
  122. package/components/SwProductListingFilters.vue +0 -292
  123. package/components/listing-filters/SwFilterPrice.vue +0 -160
  124. package/components/listing-filters/SwFilterProperties.vue +0 -123
  125. package/components/listing-filters/SwFilterRating.vue +0 -101
  126. package/components/listing-filters/SwFilterShippingFree.vue +0 -104
  127. package/components/public/cms/CmsGenericBlock.md +0 -27
  128. package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
  129. package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
  130. package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
  131. package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
  132. package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
  133. package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
  134. package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
  135. package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
  136. package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
  137. package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
  138. package/components/public/cms/element/CmsElementProductName.vue +0 -10
  139. package/components/public/cms/element/CmsElementProductSlider.vue +0 -80
  140. package/components/public/cms/element/CmsElementSidebarFilter.vue +0 -12
  141. package/components/public/cms/section/CmsSectionSidebar.vue +0 -41
  142. package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
  143. package/helpers/html-to-vue/ast.ts +0 -72
  144. package/helpers/html-to-vue/renderer.ts +0 -56
  145. /package/{components → app/components}/SwSharedPrice.vue +0 -0
  146. /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
  147. /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
  148. /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
  149. /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
  150. /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
  151. /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
  152. /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
  153. /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
  154. /package/{components → app/components}/public/cms/block/CmsBlockHtml.vue +0 -0
  155. /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
  156. /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
  157. /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
  158. /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
  159. /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
  160. /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
  161. /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
  162. /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
  163. /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
  164. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
  165. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +0 -0
  166. /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
  167. /package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +0 -0
  168. /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
  169. /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
  170. /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
  171. /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
  172. /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
  173. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
  174. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
  175. /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
  176. /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
  177. /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
  178. /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
  179. /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
  180. /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
  181. /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
  182. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
  183. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
  184. /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
  185. /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
  186. /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
  187. /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
  188. /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
  189. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
  190. /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
  191. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
  192. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
  193. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
  194. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +0 -0
  195. /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
  196. /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
  197. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
  198. /package/{helpers → app/helpers}/media/isSpatial.ts +0 -0
@@ -0,0 +1,128 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ buildUrlPrefix,
4
+ getCategoryRoute,
5
+ getTranslatedProperty,
6
+ urlIsAbsolute,
7
+ } from "@shopware/helpers";
8
+ import { computed } from "vue";
9
+ import { RouterLink } from "vue-router";
10
+ import { useUrlResolver } from "#imports";
11
+ import type { Schemas } from "#shopware";
12
+
13
+ interface Props {
14
+ navigationElement: Schemas["Category"];
15
+ isActive?: boolean;
16
+ isExpanded?: boolean;
17
+ level?: number;
18
+ }
19
+
20
+ const props = defineProps<Props>();
21
+ const emit = defineEmits<{
22
+ toggle: [];
23
+ }>();
24
+
25
+ const { getUrlPrefix } = useUrlResolver();
26
+ const url = computed(() => {
27
+ return buildUrlPrefix(
28
+ getCategoryRoute(props.navigationElement),
29
+ getUrlPrefix(),
30
+ );
31
+ });
32
+
33
+ const hasChildren = computed(() => {
34
+ return (
35
+ props.navigationElement.children &&
36
+ props.navigationElement.children.length > 0
37
+ );
38
+ });
39
+ </script>
40
+ <template>
41
+ <!-- Level 1 Category (Top-level with border and toggle) -->
42
+ <div
43
+ v-if="props.level === 0"
44
+ class="self-stretch flex flex-col justify-center items-center"
45
+ >
46
+ <div class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-start items-center gap-1">
47
+ <div class="flex-1 flex justify-start items-center gap-2.5">
48
+ <RouterLink
49
+ v-if="!urlIsAbsolute(url.path)"
50
+ :to="url"
51
+ :class="[
52
+ 'flex-1 justify-start text-surface-on-surface text-base leading-normal font-bold',
53
+ ]"
54
+ >
55
+ {{ getTranslatedProperty(navigationElement, "name") }}
56
+ </RouterLink>
57
+ <a
58
+ v-else
59
+ :href="url.path"
60
+ :class="[
61
+ 'flex-1 justify-start text-surface-on-surface text-base leading-normal font-bold',
62
+ ]"
63
+ :target="
64
+ navigationElement.externalLink || navigationElement.linkNewTab
65
+ ? '_blank'
66
+ : ''
67
+ "
68
+ >
69
+ {{ getTranslatedProperty(navigationElement, "name") }}
70
+ </a>
71
+ </div>
72
+ <button
73
+ v-if="hasChildren"
74
+ @click="emit('toggle')"
75
+ class="w-6 h-6 relative flex items-center justify-center bg-transparent cursor-pointer focus:outline-none"
76
+ type="button"
77
+ :aria-label="props.isExpanded ? 'Collapse' : 'Expand'"
78
+ >
79
+ <SwChevronIcon :direction="props.isExpanded ? 'up' : 'down'" :size="20" />
80
+ </button>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- Level 2+ Categories (Nested with left padding) -->
85
+ <div
86
+ v-else
87
+ class="self-stretch flex flex-col justify-center items-center"
88
+ >
89
+ <div class="self-stretch pl-4 py-1.5 inline-flex justify-start items-center gap-2">
90
+ <div class="py-0.5 flex-1 flex justify-start items-center gap-2.5">
91
+ <RouterLink
92
+ v-if="!urlIsAbsolute(url.path)"
93
+ :to="url"
94
+ :class="[
95
+ 'justify-start text-surface-on-surface text-base leading-normal',
96
+ props.isActive ? 'font-bold' : 'font-normal',
97
+ ]"
98
+ >
99
+ {{ getTranslatedProperty(navigationElement, "name") }}
100
+ </RouterLink>
101
+ <a
102
+ v-else
103
+ :href="url.path"
104
+ :class="[
105
+ 'justify-start text-surface-on-surface text-base leading-normal',
106
+ props.isActive ? 'font-bold' : 'font-normal',
107
+ ]"
108
+ :target="
109
+ navigationElement.externalLink || navigationElement.linkNewTab
110
+ ? '_blank'
111
+ : ''
112
+ "
113
+ >
114
+ {{ getTranslatedProperty(navigationElement, "name") }}
115
+ </a>
116
+ </div>
117
+ <button
118
+ v-if="hasChildren"
119
+ @click="emit('toggle')"
120
+ class="w-6 h-6 relative flex items-center justify-center bg-transparent cursor-pointer focus:outline-none"
121
+ type="button"
122
+ :aria-label="props.isExpanded ? 'Collapse' : 'Expand'"
123
+ >
124
+ <SwChevronIcon :direction="props.isExpanded ? 'up' : 'down'" :size="20" />
125
+ </button>
126
+ </div>
127
+ </div>
128
+ </template>
@@ -178,7 +178,7 @@ const invokeSubmit = async () => {
178
178
  id="salutation"
179
179
  v-model="state.salutationId"
180
180
  name="salutation"
181
- class="border-gray-300 focus:border-indigo-500 appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
181
+ class="border-outline-outline-variant focus:border-brand-primary appearance-none relative block w-full px-3 py-2 border placeholder-surface-on-surface-variant text-surface-on-surface rounded-md focus:outline-none focus:ring-brand-primary focus:z-10 sm:text-sm"
182
182
  >
183
183
  <option disabled selected value="">
184
184
  {{ translations.form.salutationPlaceholder }}
@@ -200,20 +200,20 @@ const invokeSubmit = async () => {
200
200
  name="first-name"
201
201
  type="text"
202
202
  autocomplete="given-name"
203
- class="appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
203
+ class="appearance-none relative block w-full px-3 py-2 border placeholder-surface-on-surface-variant text-surface-on-surface rounded-md focus:outline-none focus:ring-brand-primary focus:z-10 sm:text-sm"
204
204
  :class="[
205
205
  $v.firstName.$error
206
206
  ? 'border-red-600 focus:border-red-600'
207
- : 'border-gray-300 focus:border-indigo-500',
207
+ : 'border-outline-outline-variant focus:border-brand-primary',
208
208
  ]"
209
209
  :placeholder="translations.form.firstNamePlaceholder"
210
210
  @blur="$v.firstName.$touch()"
211
211
  />
212
212
  <span
213
- v-if="$v.firstName.$error"
213
+ v-if="$v.firstName.$error && $v.firstName.$errors[0]?.$message"
214
214
  class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
215
215
  >
216
- {{ $v.firstName.$errors[0]?.$message ?? '' }}
216
+ {{ $v.firstName.$errors[0].$message }}
217
217
  </span>
218
218
  </div>
219
219
  <div class="col-span-4">
@@ -224,20 +224,20 @@ const invokeSubmit = async () => {
224
224
  name="last-name"
225
225
  type="text"
226
226
  autocomplete="family-name"
227
- class="appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
227
+ class="appearance-none relative block w-full px-3 py-2 border placeholder-surface-on-surface-variant text-surface-on-surface rounded-md focus:outline-none focus:ring-brand-primary focus:z-10 sm:text-sm"
228
228
  :class="[
229
229
  $v.lastName.$error
230
230
  ? 'border-red-600 focus:border-red-600'
231
- : 'border-gray-300 focus:border-indigo-500',
231
+ : 'border-outline-outline-variant focus:border-brand-primary',
232
232
  ]"
233
233
  :placeholder="translations.form.lastNamePlaceholder"
234
234
  @blur="$v.lastName.$touch()"
235
235
  />
236
236
  <span
237
- v-if="$v.lastName.$error"
237
+ v-if="$v.lastName.$error && $v.lastName.$errors[0]?.$message"
238
238
  class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
239
239
  >
240
- {{ $v.lastName.$errors[0]?.$message ?? '' }}
240
+ {{ $v.lastName.$errors[0].$message }}
241
241
  </span>
242
242
  </div>
243
243
  <div class="col-span-6">
@@ -251,17 +251,17 @@ const invokeSubmit = async () => {
251
251
  :class="[
252
252
  $v.email.$error
253
253
  ? 'border-red-600 focus:border-red-600'
254
- : 'border-gray-300 focus:border-indigo-500',
254
+ : 'border-outline-outline-variant focus:border-brand-primary',
255
255
  ]"
256
- class="appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
256
+ class="appearance-none relative block w-full px-3 py-2 border placeholder-surface-on-surface-variant text-surface-on-surface rounded-md focus:outline-none focus:ring-brand-primary focus:z-10 sm:text-sm"
257
257
  :placeholder="translations.form.emailPlaceholder"
258
258
  @blur="$v.email.$touch()"
259
259
  />
260
260
  <span
261
- v-if="$v.email.$error"
261
+ v-if="$v.email.$error && $v.email.$errors[0]?.$message"
262
262
  class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
263
263
  >
264
- {{ $v.email.$errors[0]?.$message ?? '' }}
264
+ {{ $v.email.$errors[0].$message }}
265
265
  </span>
266
266
  </div>
267
267
  <div class="col-span-6">
@@ -272,20 +272,20 @@ const invokeSubmit = async () => {
272
272
  name="phone"
273
273
  type="text"
274
274
  autocomplete="phone"
275
- class="appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
275
+ class="appearance-none relative block w-full px-3 py-2 border placeholder-surface-on-surface-variant text-surface-on-surface rounded-md focus:outline-none focus:ring-brand-primary focus:z-10 sm:text-sm"
276
276
  :class="[
277
277
  $v.phone.$error
278
278
  ? 'border-red-600 focus:border-red-600'
279
- : 'border-gray-300 focus:border-indigo-500',
279
+ : 'border-outline-outline-variant focus:border-brand-primary',
280
280
  ]"
281
281
  :placeholder="translations.form.phonePlaceholder"
282
282
  @blur="$v.phone.$touch()"
283
283
  />
284
284
  <span
285
- v-if="$v.phone.$error"
285
+ v-if="$v.phone.$error && $v.phone.$errors[0]?.$message"
286
286
  class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
287
287
  >
288
- {{ $v.phone.$errors[0]?.$message ?? '' }}
288
+ {{ $v.phone.$errors[0].$message }}
289
289
  </span>
290
290
  </div>
291
291
  <div class="col-span-12">
@@ -296,20 +296,20 @@ const invokeSubmit = async () => {
296
296
  name="subject"
297
297
  type="text"
298
298
  autocomplete="subject"
299
- class="appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
299
+ class="appearance-none relative block w-full px-3 py-2 border placeholder-surface-on-surface-variant text-surface-on-surface rounded-md focus:outline-none focus:ring-brand-primary focus:z-10 sm:text-sm"
300
300
  :class="[
301
301
  $v.subject.$error
302
302
  ? 'border-red-600 focus:border-red-600'
303
- : 'border-gray-300 focus:border-indigo-500',
303
+ : 'border-outline-outline-variant focus:border-brand-primary',
304
304
  ]"
305
305
  :placeholder="translations.form.subjectPlaceholder"
306
306
  @blur="$v.subject.$touch()"
307
307
  />
308
308
  <span
309
- v-if="$v.subject.$error"
309
+ v-if="$v.subject.$error && $v.subject.$errors[0]?.$message"
310
310
  class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
311
311
  >
312
- {{ $v.subject.$errors[0]?.$message ?? '' }}
312
+ {{ $v.subject.$errors[0].$message }}
313
313
  </span>
314
314
  </div>
315
315
  <div class="col-span-12">
@@ -320,21 +320,21 @@ const invokeSubmit = async () => {
320
320
  name="comment"
321
321
  type="text"
322
322
  autocomplete="comment"
323
- class="appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:z-10 sm:text-sm"
323
+ class="appearance-none relative block w-full px-3 py-2 border placeholder-surface-on-surface-variant text-surface-on-surface rounded-md focus:outline-none focus:ring-brand-primary focus:z-10 sm:text-sm"
324
324
  :class="[
325
325
  $v.comment.$error
326
326
  ? 'border-red-600 focus:border-red-600'
327
- : 'border-gray-300 focus:border-indigo-500',
327
+ : 'border-outline-outline-variant focus:border-brand-primary',
328
328
  ]"
329
329
  :placeholder="translations.form.commentPlaceholder"
330
330
  rows="5"
331
331
  @blur="$v.comment.$touch()"
332
332
  />
333
333
  <span
334
- v-if="$v.comment.$error"
334
+ v-if="$v.comment.$error && $v.comment.$errors[0]?.$message"
335
335
  class="pt-1 text-sm text-red-600 focus:ring-brand-primary border-gray-300"
336
336
  >
337
- {{ $v.comment.$errors[0]?.$message || '' }}
337
+ {{ $v.comment.$errors[0].$message }}
338
338
  </span>
339
339
  </div>
340
340
  <div class="col-span-12">
@@ -345,7 +345,7 @@ const invokeSubmit = async () => {
345
345
  v-model="state.checkbox"
346
346
  name="privacy"
347
347
  type="checkbox"
348
- class="mt-1 focus:ring-indigo-500 h-4 w-4 border text-indigo-600 rounded"
348
+ class="mt-1 focus:ring-brand-primary h-4 w-4 border text-brand-primary rounded"
349
349
  :class="[
350
350
  $v.checkbox.$error ? 'border-red-600' : 'border-gray-300',
351
351
  ]"
@@ -363,7 +363,7 @@ const invokeSubmit = async () => {
363
363
  </div>
364
364
  <div class="flex justify-end mt-10">
365
365
  <button
366
- class="group relative flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-75"
366
+ class="group relative flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-brand-primary hover:bg-brand-primary-hover focus:outline-none focus:ring-2 focus:ring-brand-primary disabled:opacity-75"
367
367
  type="submit"
368
368
  >
369
369
  {{ translations.form.submit }}
@@ -0,0 +1,144 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import type { Schemas } from "#shopware";
4
+
5
+ type FilterState = {
6
+ manufacturer: Set<string>;
7
+ properties: Set<string>;
8
+ "min-price": number | undefined;
9
+ "max-price": number | undefined;
10
+ rating: number | undefined;
11
+ "shipping-free": boolean | undefined;
12
+ };
13
+
14
+ type Filter = {
15
+ id: string;
16
+ code: string;
17
+ options?: Array<
18
+ | Schemas["PropertyGroupOption"]
19
+ | { id: string; translated?: { name?: string } }
20
+ >;
21
+ entities?: Array<
22
+ | Schemas["ProductManufacturer"]
23
+ | { id: string; translated?: { name?: string } }
24
+ >;
25
+ };
26
+
27
+ const props = defineProps<{
28
+ filters: FilterState;
29
+ availableFilters: Filter[];
30
+ }>();
31
+
32
+ const emit = defineEmits<{
33
+ remove: [{ code: string; value: string | number }];
34
+ }>();
35
+
36
+ const getTranslatedName = (
37
+ item: { translated?: { name?: string }; name?: string } | undefined,
38
+ ): string | null => {
39
+ if (!item) return null;
40
+ if ("translated" in item) {
41
+ return item.translated?.name || ("name" in item ? item.name : null) || null;
42
+ }
43
+ return null;
44
+ };
45
+
46
+ const activeChips = computed(() => {
47
+ const chips: Array<{ label: string; code: string; value: string | number }> =
48
+ [];
49
+
50
+ // Add property filters
51
+ const properties = Array.from(props.filters.properties);
52
+ for (const propertyId of properties) {
53
+ // Check all filters, not just the one with code "properties"
54
+ // because properties can be in multiple filter groups
55
+ for (const filter of props.availableFilters) {
56
+ if ("options" in filter && filter.options) {
57
+ const option = filter.options.find((o) => o.id === propertyId);
58
+ const name = getTranslatedName(option);
59
+ if (name) {
60
+ chips.push({
61
+ label: name,
62
+ code: "properties",
63
+ value: propertyId,
64
+ });
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ // Add manufacturer filters
72
+ const manufacturers = Array.from(props.filters.manufacturer);
73
+ for (const manufacturerId of manufacturers) {
74
+ const filter = props.availableFilters.find(
75
+ (f) => f.code === "manufacturer",
76
+ );
77
+ if (filter && "entities" in filter && filter.entities) {
78
+ const entity = filter.entities.find((e) => e.id === manufacturerId);
79
+ const name = getTranslatedName(entity);
80
+ if (name) {
81
+ chips.push({
82
+ label: name,
83
+ code: "manufacturer",
84
+ value: manufacturerId,
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ // Add price filters
91
+ if (props.filters["min-price"] || props.filters["max-price"]) {
92
+ const min = props.filters["min-price"] || 0;
93
+ const max = props.filters["max-price"] || "∞";
94
+ chips.push({
95
+ label: `Price: ${min} - ${max}`,
96
+ code: "price",
97
+ value: "price-range",
98
+ });
99
+ }
100
+
101
+ // Add rating filter
102
+ if (props.filters.rating) {
103
+ chips.push({
104
+ label: `Rating: ${props.filters.rating}★`,
105
+ code: "rating",
106
+ value: props.filters.rating,
107
+ });
108
+ }
109
+
110
+ // Add shipping free filter
111
+ if (props.filters["shipping-free"]) {
112
+ chips.push({
113
+ label: "Free Shipping",
114
+ code: "shipping-free",
115
+ value: "true",
116
+ });
117
+ }
118
+
119
+ return chips;
120
+ });
121
+
122
+ const handleRemoveChip = (chip: { code: string; value: string | number }) => {
123
+ emit("remove", chip);
124
+ };
125
+ </script>
126
+
127
+ <template>
128
+ <div
129
+ v-if="activeChips.length > 0"
130
+ class="self-stretch inline-flex justify-start items-center gap-4 flex-wrap content-center mb-6"
131
+ >
132
+ <button
133
+ v-for="(chip, index) in activeChips"
134
+ :key="`${chip.code}-${chip.value}-${index}`"
135
+ @click="handleRemoveChip(chip)"
136
+ class="px-4 py-1.5 bg-brand-tertiary rounded-full inline-flex justify-center items-center gap-1 hover:bg-brand-tertiary-hover transition-colors"
137
+ >
138
+ <span class="text-brand-on-tertiary text-base font-normal leading-normal">
139
+ {{ chip.label }}
140
+ </span>
141
+ <span class="i-carbon-close w-5 h-5 text-brand-on-tertiary"></span>
142
+ </button>
143
+ </div>
144
+ </template>
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import { onClickOutside } from "@vueuse/core";
3
+ import { ref, useTemplateRef } from "vue";
4
+
5
+ defineProps<{
6
+ label: string;
7
+ isActive?: boolean;
8
+ }>();
9
+
10
+ const isOpen = ref(false);
11
+ const dropdownElement = useTemplateRef<HTMLDivElement>("dropdownElement");
12
+
13
+ onClickOutside(dropdownElement, () => {
14
+ isOpen.value = false;
15
+ });
16
+
17
+ function toggle() {
18
+ isOpen.value = !isOpen.value;
19
+ }
20
+ </script>
21
+
22
+ <template>
23
+ <div ref="dropdownElement" class="relative">
24
+ <!-- Pill button -->
25
+ <button
26
+ type="button"
27
+ class="bg-brand-tertiary rounded-full px-4 py-1.5 inline-flex items-center hover:bg-brand-tertiary-hover transition-colors"
28
+ :class="{ 'ring-2 ring-brand-primary': isActive }"
29
+ @click="toggle"
30
+ :aria-expanded="isOpen"
31
+ aria-haspopup="true"
32
+ >
33
+ <div class="py-1 inline-flex items-center gap-1">
34
+ <span class="text-brand-on-tertiary text-base font-normal leading-6">
35
+ {{ label }}
36
+ </span>
37
+ <SwChevronIcon
38
+ :direction="isOpen ? 'up' : 'down'"
39
+ :size="24"
40
+ class="text-brand-on-tertiary"
41
+ />
42
+ </div>
43
+ </button>
44
+
45
+ <!-- Dropdown panel -->
46
+ <div
47
+ v-if="isOpen"
48
+ class="absolute top-full left-0 mt-2 min-w-64 bg-surface-surface rounded-lg shadow-lg ring-1 ring-outline-outline-variant z-50 p-4"
49
+ role="menu"
50
+ >
51
+ <slot />
52
+ </div>
53
+ </div>
54
+ </template>
@@ -0,0 +1,89 @@
1
+ <script setup lang="ts">
2
+ import { useCmsTranslations, useProductPrice } from "@shopware/composables";
3
+ import { defu } from "defu";
4
+ import { toRefs } from "vue";
5
+ import type { Schemas } from "#shopware";
6
+
7
+ const props = defineProps<{
8
+ product: Schemas["Product"];
9
+ }>();
10
+
11
+ type Translations = {
12
+ listing: {
13
+ variantsFrom: string;
14
+ previously: string;
15
+ from: string;
16
+ to: string;
17
+ };
18
+ };
19
+
20
+ let translations: Translations = {
21
+ listing: {
22
+ variantsFrom: "variants from",
23
+ previously: "previously",
24
+ from: "from",
25
+ to: "to",
26
+ },
27
+ };
28
+
29
+ translations = defu(useCmsTranslations(), translations) as Translations;
30
+
31
+ const { product } = toRefs(props);
32
+
33
+ const {
34
+ price,
35
+ unitPrice,
36
+ displayFromVariants,
37
+ displayFrom,
38
+ hasListPrice,
39
+ regulationPrice,
40
+ } = useProductPrice(product);
41
+ </script>
42
+
43
+ <template>
44
+ <div :id="product.id" class="inline-flex justify-start items-center gap-2">
45
+ <!-- Sale price display -->
46
+ <div v-if="hasListPrice" class="flex items-center gap-2">
47
+ <div class="text-base font-bold leading-normal">
48
+ <SwSharedPrice :value="unitPrice">
49
+ <template #beforePrice>
50
+ <span v-if="displayFrom || displayFromVariants" class="text-sm">{{
51
+ translations.listing.from
52
+ }}</span>
53
+ </template>
54
+ </SwSharedPrice>
55
+ </div>
56
+ <div class="text-surface-on-surface-variant text-sm font-normal leading-tight line-through">
57
+ <SwSharedPrice :value="price?.listPrice?.price" />
58
+ </div>
59
+ </div>
60
+
61
+ <!-- Regular price display -->
62
+ <div v-else class="text-surface-on-surface text-base font-bold leading-normal">
63
+ <SwSharedPrice :value="unitPrice">
64
+ <template #beforePrice>
65
+ <span v-if="displayFrom || displayFromVariants" class="text-sm">{{
66
+ translations.listing.from
67
+ }}</span>
68
+ </template>
69
+ </SwSharedPrice>
70
+ </div>
71
+
72
+ <!-- Variants from price -->
73
+ <div v-if="displayFromVariants" class="text-surface-on-surface text-base font-bold leading-normal">
74
+ <SwSharedPrice :value="displayFromVariants">
75
+ <template #beforePrice>
76
+ <span v-if="displayFromVariants" class="text-sm">{{
77
+ translations.listing.variantsFrom
78
+ }}</span>
79
+ </template>
80
+ </SwSharedPrice>
81
+ </div>
82
+
83
+ <!-- Regulation price -->
84
+ <div v-if="regulationPrice" class="flex gap-2 text-surface-on-surface-variant text-sm">
85
+ {{ translations.listing.previously }}
86
+ <SwSharedPrice :value="regulationPrice" />
87
+ </div>
88
+ </div>
89
+ </template>
@@ -2,6 +2,7 @@
2
2
  import { OrbitControls, useGLTF } from "@tresjs/cientos";
3
3
  import { TresCanvas } from "@tresjs/core";
4
4
  import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from "three";
5
+ import { computed } from "vue";
5
6
 
6
7
  const props = defineProps<{
7
8
  src: string;
@@ -17,7 +18,8 @@ const gl = {
17
18
  windowSize: false,
18
19
  };
19
20
 
20
- const { scene: model } = await useGLTF(props.src);
21
+ const { state } = await useGLTF(props.src);
22
+ const model = computed(() => state.value?.scene);
21
23
  </script>
22
24
  <template>
23
25
  <TresCanvas v-bind="gl">
@@ -27,7 +29,7 @@ const { scene: model } = await useGLTF(props.src);
27
29
  :look-at="[0, 0, 0]"
28
30
  />
29
31
  <OrbitControls />
30
- <primitive :object="model" />
32
+ <primitive v-if="model" :object="model" />
31
33
  <TresDirectionalLight :position="[3, 3, 3]" :intensity="1" />
32
34
  <TresAmbientLight :intensity="2" />
33
35
  </TresCanvas>