@shopware/cms-base-layer 1.5.1 → 2.0.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 (183) hide show
  1. package/README.md +330 -12
  2. package/app/app.config.ts +7 -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 +76 -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/SwListingProductPrice.vue +89 -0
  15. package/{components → app/components}/SwNewsletterForm.vue +45 -34
  16. package/{components → app/components}/SwPagination.vue +3 -5
  17. package/{components → app/components}/SwProductAddToCart.vue +22 -27
  18. package/app/components/SwProductCard.vue +170 -0
  19. package/app/components/SwProductCardDetails.vue +57 -0
  20. package/app/components/SwProductCardImage.vue +87 -0
  21. package/app/components/SwProductCardSkeleton.vue +33 -0
  22. package/app/components/SwProductListingFilter.vue +64 -0
  23. package/app/components/SwProductListingFilters.vue +308 -0
  24. package/{components → app/components}/SwProductReviews.vue +28 -13
  25. package/app/components/SwProductReviewsForm.vue +292 -0
  26. package/app/components/SwQuantitySelect.vue +106 -0
  27. package/{components → app/components}/SwSlider.vue +4 -4
  28. package/app/components/SwSortDropdown.vue +83 -0
  29. package/app/components/SwStockInfo.vue +44 -0
  30. package/{components → app/components}/SwVariantConfigurator.vue +1 -1
  31. package/app/components/listing-filters/SwFilterPrice.vue +214 -0
  32. package/app/components/listing-filters/SwFilterProperties.vue +113 -0
  33. package/app/components/listing-filters/SwFilterRating.vue +90 -0
  34. package/app/components/listing-filters/SwFilterShippingFree.vue +107 -0
  35. package/{components → app/components}/public/cms/CmsPage.vue +19 -4
  36. package/{components → app/components}/public/cms/block/CmsBlockGalleryBuybox.vue +5 -5
  37. package/{components → app/components}/public/cms/block/CmsBlockImageBubbleRow.vue +5 -5
  38. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +41 -0
  39. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +42 -0
  40. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +37 -0
  41. package/{components → app/components}/public/cms/block/CmsBlockImageSimpleGrid.vue +11 -5
  42. package/{components → app/components}/public/cms/block/CmsBlockImageText.vue +7 -3
  43. package/{components → app/components}/public/cms/block/CmsBlockImageTextBubble.vue +13 -16
  44. package/{components → app/components}/public/cms/block/CmsBlockImageTextCover.vue +7 -9
  45. package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +88 -0
  46. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +53 -0
  47. package/{components → app/components}/public/cms/block/CmsBlockImageThreeColumn.vue +10 -4
  48. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +37 -0
  49. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +37 -0
  50. package/{components → app/components}/public/cms/block/CmsBlockProductHeading.vue +1 -1
  51. package/{components → app/components}/public/cms/block/CmsBlockProductThreeColumn.vue +10 -4
  52. package/{components → app/components}/public/cms/block/CmsBlockSidebarFilter.vue +3 -1
  53. package/app/components/public/cms/block/CmsBlockTextOnImage.vue +30 -0
  54. package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
  55. package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
  56. package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +3 -3
  57. package/{components → app/components}/public/cms/element/CmsElementImage.vue +52 -13
  58. package/app/components/public/cms/element/CmsElementImageGallery.vue +158 -0
  59. package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
  60. package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +2 -1
  61. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
  62. package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +23 -94
  63. package/app/components/public/cms/element/CmsElementProductName.vue +11 -0
  64. package/{components → app/components}/public/cms/element/CmsElementProductSlider.vue +4 -4
  65. package/{components → app/components}/public/cms/element/CmsElementText.vue +8 -2
  66. package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
  67. package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +1 -1
  68. package/app/components/public/cms/section/CmsSectionSidebar.vue +36 -0
  69. package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
  70. package/app/components/ui/BaseButton.vue +99 -0
  71. package/app/components/ui/BaseIcon.vue +15 -0
  72. package/app/components/ui/Checkbox.vue +49 -0
  73. package/app/components/ui/CheckmarkIcon.vue +23 -0
  74. package/app/components/ui/ChevronIcon.vue +37 -0
  75. package/app/components/ui/ExclamationIcon.vue +11 -0
  76. package/app/components/ui/IconButton.vue +32 -0
  77. package/app/components/ui/RadioButton.vue +26 -0
  78. package/app/components/ui/StarIcon.vue +18 -0
  79. package/app/components/ui/SwitchButton.vue +100 -0
  80. package/app/components/ui/UserIcon.vue +11 -0
  81. package/app/components/ui/WishlistIcon.vue +20 -0
  82. package/app/composables/useImagePlaceholder.ts +27 -0
  83. package/{helpers → app/helpers}/clientOnly.ts +5 -0
  84. package/app/providers/shopware.test.ts +213 -0
  85. package/app/providers/shopware.ts +107 -0
  86. package/dist/index.d.mts +3 -3
  87. package/dist/index.d.ts +3 -3
  88. package/dist/index.mjs +2 -2
  89. package/index.d.ts +12 -0
  90. package/nuxt.config.ts +80 -6
  91. package/package.json +29 -21
  92. package/uno.config.ts +83 -0
  93. package/components/SwCategoryNavigation.vue +0 -44
  94. package/components/SwCategoryNavigationLink.vue +0 -57
  95. package/components/SwListingProductPrice.vue +0 -89
  96. package/components/SwProductCard.vue +0 -286
  97. package/components/SwProductListingFilter.vue +0 -42
  98. package/components/SwProductListingFilters.vue +0 -292
  99. package/components/listing-filters/SwFilterPrice.vue +0 -160
  100. package/components/listing-filters/SwFilterProperties.vue +0 -123
  101. package/components/listing-filters/SwFilterRating.vue +0 -101
  102. package/components/listing-filters/SwFilterShippingFree.vue +0 -104
  103. package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
  104. package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
  105. package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
  106. package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
  107. package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
  108. package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
  109. package/components/public/cms/block/CmsBlockTextOnImage.vue +0 -20
  110. package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
  111. package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
  112. package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
  113. package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
  114. package/components/public/cms/element/CmsElementProductName.vue +0 -10
  115. package/components/public/cms/section/CmsSectionSidebar.vue +0 -41
  116. package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
  117. /package/{components → app/components}/SwMedia3D.vue +0 -0
  118. /package/{components → app/components}/SwProductGallery.vue +0 -0
  119. /package/{components → app/components}/SwProductPrice.vue +0 -0
  120. /package/{components → app/components}/SwProductUnits.vue +0 -0
  121. /package/{components → app/components}/SwSharedPrice.vue +0 -0
  122. /package/{components → app/components}/public/cms/CmsGenericBlock.md +0 -0
  123. /package/{components → app/components}/public/cms/CmsGenericBlock.vue +0 -0
  124. /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
  125. /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
  126. /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
  127. /package/{components → app/components}/public/cms/CmsPage.md +0 -0
  128. /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
  129. /package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +0 -0
  130. /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
  131. /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
  132. /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
  133. /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
  134. /package/{components → app/components}/public/cms/block/CmsBlockHtml.vue +0 -0
  135. /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
  136. /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
  137. /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
  138. /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
  139. /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
  140. /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
  141. /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
  142. /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
  143. /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
  144. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
  145. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +0 -0
  146. /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
  147. /package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +0 -0
  148. /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
  149. /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
  150. /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
  151. /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
  152. /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
  153. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
  154. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
  155. /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
  156. /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
  157. /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
  158. /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
  159. /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
  160. /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
  161. /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
  162. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
  163. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
  164. /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
  165. /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
  166. /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
  167. /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
  168. /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
  169. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
  170. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.vue +0 -0
  171. /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
  172. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
  173. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
  174. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
  175. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +0 -0
  176. /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
  177. /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
  178. /package/{helpers → app/helpers}/html-to-vue/ast.ts +0 -0
  179. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
  180. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +0 -0
  181. /package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +0 -0
  182. /package/{helpers → app/helpers}/html-to-vue/renderer.ts +0 -0
  183. /package/{helpers → app/helpers}/media/isSpatial.ts +0 -0
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ const model = defineModel<boolean>({
3
+ required: true,
4
+ });
5
+
6
+ const {
7
+ label,
8
+ description,
9
+ disabled = false,
10
+ } = defineProps<{
11
+ label?: string;
12
+ description?: string;
13
+ disabled?: boolean;
14
+ }>();
15
+ </script>
16
+
17
+ <template>
18
+ <label class="flex items-start gap-2">
19
+ <input
20
+ class="accent-brand-primary w-4 h-4 focus-within:outline-2 focus-within:outline-brand-primary focus-within:outline focus-within:outline-offset-[2px] focus-within"
21
+ type="checkbox"
22
+ v-model="model"
23
+ :disabled
24
+ />
25
+ <div v-if="label || description">
26
+ <p
27
+ v-if="label"
28
+ :class="
29
+ disabled
30
+ ? 'text-surface-on-surface-disabled'
31
+ : 'text-surface-on-surface'
32
+ "
33
+ >
34
+ {{ label }}
35
+ </p>
36
+ <p
37
+ v-if="description"
38
+ class="text-sm"
39
+ :class="
40
+ disabled
41
+ ? 'text-surface-on-surface-disabled'
42
+ : 'text-surface-on-surface-variant'
43
+ "
44
+ >
45
+ {{ description }}
46
+ </p>
47
+ </div>
48
+ </label>
49
+ </template>
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ import CheckmarkFilledSvg from "@cms-assets/icons/check-circle.svg";
3
+ import CheckmarkSvg from "@cms-assets/icons/checkmark.svg";
4
+
5
+ const {
6
+ filled = false,
7
+ size = 24,
8
+ alt = "",
9
+ } = defineProps<{
10
+ filled?: boolean;
11
+ size?: number;
12
+ alt?: string;
13
+ }>();
14
+ </script>
15
+
16
+ <template>
17
+ <NuxtImg
18
+ :src="filled ? CheckmarkFilledSvg : CheckmarkSvg"
19
+ :alt="alt"
20
+ :width="size"
21
+ :height="size"
22
+ />
23
+ </template>
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ import ChevronSvg from "@cms-assets/icons/chevron.svg";
3
+ import { computed } from "vue";
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ direction?: "up" | "down" | "left" | "right";
8
+ size?: number;
9
+ alt?: string;
10
+ }>(),
11
+ {
12
+ direction: "down",
13
+ size: 24,
14
+ alt: "",
15
+ },
16
+ );
17
+
18
+ const rotationClass = computed(() => {
19
+ const rotations = {
20
+ down: "",
21
+ up: "rotate-180",
22
+ left: "rotate-90",
23
+ right: "-rotate-90",
24
+ };
25
+ return rotations[props.direction];
26
+ });
27
+ </script>
28
+
29
+ <template>
30
+ <NuxtImg
31
+ :src="ChevronSvg"
32
+ :alt="alt"
33
+ :class="['transition-transform', rotationClass]"
34
+ :width="size"
35
+ :height="size"
36
+ />
37
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ import ExclamationCircleSvg from "@cms-assets/icons/exclamation-circle.svg";
3
+
4
+ const { size = 24 } = defineProps<{
5
+ size?: number;
6
+ }>();
7
+ </script>
8
+
9
+ <template>
10
+ <SwBaseIcon :src="ExclamationCircleSvg" :size="size" alt="Error" />
11
+ </template>
@@ -0,0 +1,32 @@
1
+ <script lang="ts" setup>
2
+ const { type = "primary" } = defineProps<{
3
+ type?: "primary" | "secondary" | "tertiary" | "outline" | "ghost";
4
+ }>();
5
+
6
+ const styles = {
7
+ primary:
8
+ "bg-brand-primary hover:focus:bg-brand-primary-hover text-brand-on-primary",
9
+ secondary:
10
+ "bg-brand-secondary hover:focus:bg-brand-secondary-hover text-brand-on-secondary",
11
+ tertiary:
12
+ "bg-brand-tertiary hover:focus:bg-brand-tertiary-hover text-brand-on-tertiary",
13
+ outline:
14
+ "text-brand-primary bg-transparent hover:focus:bg-surface-surface-container outline outline-2 outline-offset-[-2px] outline-brand-primary",
15
+ ghost: "bg-transparent hover:focus:bg-surface-surface-container",
16
+ };
17
+ </script>
18
+
19
+ <template>
20
+ <button
21
+ :class="[
22
+ styles[type],
23
+ {
24
+ 'bg-surface-on-surface-disabled text-surface-surface-disabled':
25
+ $attrs.disabled,
26
+ 'w-10 h-10': type !== 'ghost',
27
+ },
28
+ ]"
29
+ >
30
+ <slot />
31
+ </button>
32
+ </template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ const modelValue = defineModel<string | null>();
3
+
4
+ const { selected } = defineProps<{
5
+ selected: boolean;
6
+ }>();
7
+ </script>
8
+ <template>
9
+ <input
10
+ type="radio"
11
+ class="sr-only"
12
+ v-bind="$attrs"
13
+ v-model="modelValue"
14
+ name="shipping-method"
15
+ />
16
+ <div
17
+ class="w-4 h-4 rounded-full border border-outline-outline border-spacing-1 flex items-center justify-center"
18
+ >
19
+ <div
20
+ :class="{
21
+ 'bg-brand-primary': selected,
22
+ }"
23
+ class="w-2.5 h-2.5 rounded-full"
24
+ ></div>
25
+ </div>
26
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import StarEmptySvg from "@cms-assets/icons/star-empty.svg";
3
+ import StarFilledSvg from "@cms-assets/icons/star-filled.svg";
4
+
5
+ const { filled = true, size = 20 } = defineProps<{
6
+ filled?: boolean;
7
+ size?: number;
8
+ }>();
9
+ </script>
10
+
11
+ <template>
12
+ <NuxtImg
13
+ :src="filled ? StarFilledSvg : StarEmptySvg"
14
+ alt="Star"
15
+ :width="size"
16
+ :height="size"
17
+ />
18
+ </template>
@@ -0,0 +1,100 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from "vue";
3
+
4
+ const props = defineProps<{
5
+ name?: string;
6
+ ariaLabel?: string;
7
+ label?: string;
8
+ description?: string;
9
+ disabled?: boolean;
10
+ }>();
11
+
12
+ const modelValue = defineModel<boolean | null>();
13
+
14
+ const emits = defineEmits<{
15
+ change: [v: boolean | null];
16
+ }>();
17
+
18
+ const value = computed(() => !!modelValue.value);
19
+
20
+ const localChecked = ref<boolean>(value.value);
21
+ watch(value, (v) => {
22
+ localChecked.value = v;
23
+ });
24
+
25
+ const inputName = props.name ?? "switch-button";
26
+ const inputId = `switch-${inputName}`;
27
+ const inputRef = ref<HTMLInputElement | null>(null);
28
+
29
+ const activateByKeyboard = () => {
30
+ inputRef.value?.click();
31
+ };
32
+ const toggleState = (next?: boolean) => {
33
+ if (props.disabled) return;
34
+ const v = typeof next === "boolean" ? next : !localChecked.value;
35
+ localChecked.value = v;
36
+ modelValue.value = v;
37
+ emits("change", v);
38
+ };
39
+
40
+ const activateByClick = (ev?: Event) => {
41
+ ev?.stopPropagation();
42
+ toggleState();
43
+ };
44
+ </script>
45
+
46
+ <template>
47
+ <div class="w-full inline-flex flex-col justify-start items-start gap-2">
48
+ <div class="self-stretch inline-flex justify-start items-center gap-3">
49
+ <!-- left label that toggles the input via for="#inputId" -->
50
+ <label :for="inputId"
51
+ class="flex-1 flex justify-start items-center gap-1 text-surface-on-surface text-base font-normal leading-normal cursor-pointer"
52
+ :class="{ 'cursor-not-allowed': disabled }">
53
+ <span v-if="$slots.default">
54
+ <slot />
55
+ </span>
56
+ <span v-else-if="label">{{ label }}</span>
57
+ </label>
58
+
59
+ <div class="w-10 h-6 relative">
60
+ <label class="inline-block cursor-pointer" :class="{ 'cursor-not-allowed': disabled }">
61
+ <input ref="inputRef" :id="inputId" type="checkbox" :name="inputName" class="sr-only" :checked="localChecked"
62
+ @change="toggleState()"
63
+ :disabled="disabled" :aria-label="ariaLabel || undefined" v-bind="$attrs" />
64
+ <span role="switch" :aria-checked="localChecked" :tabindex="disabled ? -1 : 0"
65
+ class="w-10 h-6 relative rounded-full flex-shrink-0 inline-block switch-track cursor-pointer"
66
+ :class="localChecked ? 'bg-brand-secondary switch-track--on' : 'bg-surface-surface-container-highest'"
67
+ @keydown.space.prevent="activateByKeyboard" @click.prevent="activateByClick">
68
+ <span class="w-4 h-4 rounded-full absolute switch-knob"
69
+ :style="{ left: localChecked ? '19px' : '4px', top: '4px' }"
70
+ :class="localChecked ? 'bg-brand-on-secondary' : 'bg-surface-on-surface-variant'"></span>
71
+ </span>
72
+ </label>
73
+ </div>
74
+ </div>
75
+ <div v-if="description || $slots.description" class="self-stretch inline-flex justify-start items-center gap-2.5">
76
+ <div class="flex-1 justify-start text-surface-on-surface-variant text-sm font-normal leading-tight">
77
+ <slot name="description">{{ description }}</slot>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </template>
82
+
83
+ <style scoped>
84
+ .switch-track {
85
+ transition: background-color 180ms ease-in-out, box-shadow 180ms ease-in-out;
86
+ }
87
+
88
+ .switch-track--on {
89
+ box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.08);
90
+ /* subtle glow when on */
91
+ }
92
+
93
+ .switch-knob {
94
+ transition: left 180ms cubic-bezier(.2, .9, .2, 1), top 180ms cubic-bezier(.2, .9, .2, 1), background-color 120ms linear;
95
+ }
96
+
97
+ .switch-track:focus {
98
+ outline: none;
99
+ }
100
+ </style>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ import UserSvg from "@cms-assets/icons/user.svg";
3
+
4
+ const { size = 24 } = defineProps<{
5
+ size?: number;
6
+ }>();
7
+ </script>
8
+
9
+ <template>
10
+ <SwBaseIcon :src="UserSvg" :size="size" alt="User" />
11
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ withDefaults(
3
+ defineProps<{
4
+ filled?: boolean;
5
+ }>(),
6
+ {
7
+ filled: false,
8
+ },
9
+ );
10
+ </script>
11
+ <template>
12
+ <div class="relative">
13
+ <!-- use when filled icon is ready -->
14
+ <!-- <Icon size="1rem" name="shopware:heart" v-if="type !== 'filled'" class="w-6 h-5 block hover:cursor-pointer" :class="[
15
+ styles[type]
16
+ ]" /> -->
17
+ <div class="i-carbon-favorite w-6 h-5 hover:cursor-pointer" v-if="!filled"></div>
18
+ <div class="i-carbon-favorite-filled w-6 h-5 hover:cursor-pointer" v-else></div>
19
+ </div>
20
+ </template>
@@ -0,0 +1,27 @@
1
+ import { useAppConfig } from "nuxt/app";
2
+
3
+ /**
4
+ * Composable that provides an SVG placeholder image as a data URI
5
+ * Note: CSS variables and currentColor don't work in data URIs since SVGs are rendered in isolation
6
+ *
7
+ * @param color - Hex color for the placeholder (optional - defaults to appConfig.imagePlaceholder.color or #543B95)
8
+ * @returns Base64-encoded SVG data URI
9
+ */
10
+ export function useImagePlaceholder(color?: string) {
11
+ const appConfig = useAppConfig();
12
+ const placeholderColor =
13
+ color || appConfig.imagePlaceholder?.color || "#543B95";
14
+
15
+ const placeholderSvg = `data:image/svg+xml;base64,${btoa(
16
+ `
17
+ <svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
18
+ <rect width="96" height="96" rx="8" fill="${placeholderColor}" opacity="0.08"/>
19
+ <g transform="translate(36, 36)">
20
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M3 22H21C21.5523 22 22 21.5523 22 21V17L17.7071 12.7071C17.3166 12.3166 16.6834 12.3166 16.2929 12.7071L10.5 18.5C10.2239 18.7761 9.77614 18.7761 9.5 18.5C9.22386 18.2239 9.22386 17.7761 9.5 17.5L11 16L8.70711 13.7071C8.31658 13.3166 7.68342 13.3166 7.29289 13.7071L2 19V21C2 21.5523 2.44772 22 3 22ZM21 24H3C1.34315 24 0 22.6569 0 21V3C0 1.34315 1.34315 0 3 0H21C22.6569 0 24 1.34315 24 3V21C24 22.6569 22.6569 24 21 24ZM6.5 9C7.88071 9 9 7.88071 9 6.5C9 5.11929 7.88071 4 6.5 4C5.11929 4 4 5.11929 4 6.5C4 7.88071 5.11929 9 6.5 9Z" fill="${placeholderColor}" opacity="0.4"/>
21
+ </g>
22
+ </svg>
23
+ `.trim(),
24
+ )}`;
25
+
26
+ return placeholderSvg;
27
+ }
@@ -1,5 +1,10 @@
1
1
  import { defineComponent, onMounted, ref } from "vue";
2
2
 
3
+ /**
4
+ * @deprecated This component is deprecated and will be removed in the next major release.
5
+ * Use Nuxt's built-in `<ClientOnly>` component instead.
6
+ * @see https://nuxt.com/docs/api/components/client-only
7
+ */
3
8
  export const ClientOnly = defineComponent({
4
9
  setup(_, { slots }) {
5
10
  const init = ref(false);
@@ -0,0 +1,213 @@
1
+ import type { ImageCTX } from "@nuxt/image";
2
+ import { describe, expect, it } from "vitest";
3
+ import shopwareProvider from "./shopware";
4
+
5
+ describe("Shopware Image Provider", () => {
6
+ const provider = shopwareProvider();
7
+ const mockContext: ImageCTX = {
8
+ options: {},
9
+ } as ImageCTX;
10
+
11
+ describe("basic functionality", () => {
12
+ it("should return original URL when no modifiers are provided", () => {
13
+ const result = provider.getImage(
14
+ "https://example.com/image.jpg",
15
+ { modifiers: {} },
16
+ mockContext,
17
+ );
18
+ expect(result.url).toBe("https://example.com/image.jpg");
19
+ });
20
+
21
+ it("should add width modifier to URL", () => {
22
+ const result = provider.getImage(
23
+ "https://example.com/image.jpg",
24
+ { modifiers: { width: 800 } },
25
+ mockContext,
26
+ );
27
+ expect(result.url).toBe("https://example.com/image.jpg?width=800");
28
+ });
29
+
30
+ it("should add height modifier to URL", () => {
31
+ const result = provider.getImage(
32
+ "https://example.com/image.jpg",
33
+ { modifiers: { height: 600 } },
34
+ mockContext,
35
+ );
36
+ expect(result.url).toBe("https://example.com/image.jpg?height=600");
37
+ });
38
+
39
+ it("should add multiple modifiers to URL", () => {
40
+ const result = provider.getImage(
41
+ "https://example.com/image.jpg",
42
+ {
43
+ modifiers: {
44
+ width: 800,
45
+ height: 600,
46
+ quality: 90,
47
+ format: "webp",
48
+ fit: "cover",
49
+ },
50
+ },
51
+ mockContext,
52
+ );
53
+ expect(result.url).toBe(
54
+ "https://example.com/image.jpg?width=800&height=600&quality=90&format=webp&fit=cover",
55
+ );
56
+ });
57
+ });
58
+
59
+ describe("existing query parameters", () => {
60
+ it("should append modifiers to existing query parameters", () => {
61
+ const result = provider.getImage(
62
+ "https://example.com/image.jpg?ts=123456",
63
+ { modifiers: { width: 800 } },
64
+ mockContext,
65
+ );
66
+ expect(result.url).toBe(
67
+ "https://example.com/image.jpg?ts=123456&width=800",
68
+ );
69
+ });
70
+
71
+ it("should handle multiple existing query parameters", () => {
72
+ const result = provider.getImage(
73
+ "https://example.com/image.jpg?ts=123456&v=2",
74
+ { modifiers: { width: 800, format: "webp" } },
75
+ mockContext,
76
+ );
77
+ expect(result.url).toBe(
78
+ "https://example.com/image.jpg?ts=123456&v=2&width=800&format=webp",
79
+ );
80
+ });
81
+ });
82
+
83
+ describe("special characters in URL", () => {
84
+ it("should encode commas in pathname", () => {
85
+ const result = provider.getImage(
86
+ "https://example.com/path/image, test.jpg",
87
+ { modifiers: { width: 800 } },
88
+ mockContext,
89
+ );
90
+ expect(result.url).toBe(
91
+ "https://example.com/path/image%2C%20test.jpg?width=800",
92
+ );
93
+ });
94
+
95
+ it("should encode spaces in pathname", () => {
96
+ const result = provider.getImage(
97
+ "https://example.com/path/image test.jpg",
98
+ { modifiers: { width: 800 } },
99
+ mockContext,
100
+ );
101
+ expect(result.url).toBe(
102
+ "https://example.com/path/image%20test.jpg?width=800",
103
+ );
104
+ });
105
+
106
+ it("should encode special characters in filename", () => {
107
+ const result = provider.getImage(
108
+ "https://cdn.shopware.store/media/ChatGPT Image 2 gru 2025, 14_08_58.png",
109
+ { modifiers: { height: 300, format: "webp" } },
110
+ mockContext,
111
+ );
112
+ expect(result.url).toBe(
113
+ "https://cdn.shopware.store/media/ChatGPT%20Image%202%20gru%202025%2C%2014_08_58.png?height=300&format=webp",
114
+ );
115
+ });
116
+
117
+ it("should handle already encoded URLs correctly", () => {
118
+ const result = provider.getImage(
119
+ "https://example.com/path/image%20test.jpg",
120
+ { modifiers: { width: 800 } },
121
+ mockContext,
122
+ );
123
+ expect(result.url).toBe(
124
+ "https://example.com/path/image%20test.jpg?width=800",
125
+ );
126
+ });
127
+
128
+ it("should encode parentheses in pathname", () => {
129
+ const result = provider.getImage(
130
+ "https://example.com/path/image (1).jpg",
131
+ { modifiers: { width: 800 } },
132
+ mockContext,
133
+ );
134
+ expect(result.url).toBe(
135
+ "https://example.com/path/image%20(1).jpg?width=800",
136
+ );
137
+ });
138
+ });
139
+
140
+ describe("edge cases", () => {
141
+ it("should handle relative URLs gracefully", () => {
142
+ const result = provider.getImage(
143
+ "/media/image.jpg",
144
+ { modifiers: { width: 800 } },
145
+ mockContext,
146
+ );
147
+ // Relative URLs cannot be parsed as URL objects, so they are returned as-is with modifiers
148
+ expect(result.url).toBe("/media/image.jpg?width=800");
149
+ });
150
+
151
+ it("should preserve existing query parameters with special characters", () => {
152
+ const result = provider.getImage(
153
+ "https://example.com/image.jpg?ts=123456&key=value",
154
+ { modifiers: { width: 800 } },
155
+ mockContext,
156
+ );
157
+ expect(result.url).toBe(
158
+ "https://example.com/image.jpg?ts=123456&key=value&width=800",
159
+ );
160
+ });
161
+
162
+ it("should handle URLs with fragments", () => {
163
+ const result = provider.getImage(
164
+ "https://example.com/image.jpg#section",
165
+ { modifiers: { width: 800 } },
166
+ mockContext,
167
+ );
168
+ // Note: URL constructor places hash after query params
169
+ expect(result.url).toBe(
170
+ "https://example.com/image.jpg#section?width=800",
171
+ );
172
+ });
173
+
174
+ it("should not double-encode already encoded characters", () => {
175
+ const result = provider.getImage(
176
+ "https://example.com/path/image%2Ctest.jpg",
177
+ { modifiers: { width: 800 } },
178
+ mockContext,
179
+ );
180
+ expect(result.url).toBe(
181
+ "https://example.com/path/image%2Ctest.jpg?width=800",
182
+ );
183
+ });
184
+ });
185
+
186
+ describe("Shopware CDN URLs", () => {
187
+ it("should handle Shopware CDN URLs with existing query parameters", () => {
188
+ const result = provider.getImage(
189
+ "https://cdn.shopware.store/a/B/m/pPkDE/media/51/69/bf/1764680961/image.png?width=280&ts=1764680961",
190
+ {
191
+ modifiers: { height: 300, format: "webp", quality: 90, fit: "cover" },
192
+ },
193
+ mockContext,
194
+ );
195
+ expect(result.url).toBe(
196
+ "https://cdn.shopware.store/a/B/m/pPkDE/media/51/69/bf/1764680961/image.png?width=280&ts=1764680961&height=300&quality=90&format=webp&fit=cover",
197
+ );
198
+ });
199
+
200
+ it("should properly encode commas in Shopware CDN URLs", () => {
201
+ const result = provider.getImage(
202
+ "https://cdn.shopware.store/a/B/m/pPkDE/media/51/69/bf/1764680961/ChatGPT Image 2 gru 2025, 14_08_58.png?width=280&ts=1764680961",
203
+ {
204
+ modifiers: { height: 300, format: "webp", quality: 90, fit: "cover" },
205
+ },
206
+ mockContext,
207
+ );
208
+ expect(result.url).toBe(
209
+ "https://cdn.shopware.store/a/B/m/pPkDE/media/51/69/bf/1764680961/ChatGPT%20Image%202%20gru%202025%2C%2014_08_58.png?width=280&ts=1764680961&height=300&quality=90&format=webp&fit=cover",
210
+ );
211
+ });
212
+ });
213
+ });