@shopware/cms-base-layer 1.5.0 → 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 (184) hide show
  1. package/README.md +328 -13
  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/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +4 -4
  55. package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +3 -5
  56. package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
  57. package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
  58. package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +3 -3
  59. package/{components → app/components}/public/cms/element/CmsElementImage.vue +52 -13
  60. package/app/components/public/cms/element/CmsElementImageGallery.vue +158 -0
  61. package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
  62. package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +2 -1
  63. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
  64. package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +23 -94
  65. package/app/components/public/cms/element/CmsElementProductName.vue +11 -0
  66. package/{components → app/components}/public/cms/element/CmsElementProductSlider.vue +4 -4
  67. package/{components → app/components}/public/cms/element/CmsElementText.vue +8 -2
  68. package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +8 -2
  69. package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
  70. package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +1 -1
  71. package/app/components/public/cms/section/CmsSectionSidebar.vue +36 -0
  72. package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
  73. package/app/components/ui/BaseButton.vue +99 -0
  74. package/app/components/ui/BaseIcon.vue +15 -0
  75. package/app/components/ui/Checkbox.vue +49 -0
  76. package/app/components/ui/CheckmarkIcon.vue +23 -0
  77. package/app/components/ui/ChevronIcon.vue +37 -0
  78. package/app/components/ui/ExclamationIcon.vue +11 -0
  79. package/app/components/ui/IconButton.vue +32 -0
  80. package/app/components/ui/RadioButton.vue +26 -0
  81. package/app/components/ui/StarIcon.vue +18 -0
  82. package/app/components/ui/SwitchButton.vue +100 -0
  83. package/app/components/ui/UserIcon.vue +11 -0
  84. package/app/components/ui/WishlistIcon.vue +20 -0
  85. package/app/composables/useImagePlaceholder.ts +27 -0
  86. package/{helpers → app/helpers}/clientOnly.ts +5 -0
  87. package/app/providers/shopware.test.ts +213 -0
  88. package/app/providers/shopware.ts +107 -0
  89. package/dist/index.d.mts +3 -3
  90. package/dist/index.d.ts +3 -3
  91. package/dist/index.mjs +2 -2
  92. package/index.d.ts +12 -0
  93. package/nuxt.config.ts +80 -6
  94. package/package.json +29 -21
  95. package/uno.config.ts +83 -0
  96. package/components/SwCategoryNavigation.vue +0 -44
  97. package/components/SwCategoryNavigationLink.vue +0 -57
  98. package/components/SwListingProductPrice.vue +0 -89
  99. package/components/SwProductCard.vue +0 -286
  100. package/components/SwProductListingFilter.vue +0 -42
  101. package/components/SwProductListingFilters.vue +0 -292
  102. package/components/listing-filters/SwFilterPrice.vue +0 -160
  103. package/components/listing-filters/SwFilterProperties.vue +0 -123
  104. package/components/listing-filters/SwFilterRating.vue +0 -101
  105. package/components/listing-filters/SwFilterShippingFree.vue +0 -104
  106. package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
  107. package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
  108. package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
  109. package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
  110. package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
  111. package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
  112. package/components/public/cms/block/CmsBlockTextOnImage.vue +0 -20
  113. package/components/public/cms/element/CmsBlockHtml.md +0 -1
  114. package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
  115. package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
  116. package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
  117. package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
  118. package/components/public/cms/element/CmsElementProductName.vue +0 -10
  119. package/components/public/cms/section/CmsSectionSidebar.vue +0 -49
  120. package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
  121. /package/{components → app/components}/SwMedia3D.vue +0 -0
  122. /package/{components → app/components}/SwProductGallery.vue +0 -0
  123. /package/{components → app/components}/SwProductPrice.vue +0 -0
  124. /package/{components → app/components}/SwProductUnits.vue +0 -0
  125. /package/{components → app/components}/SwSharedPrice.vue +0 -0
  126. /package/{components → app/components}/public/cms/CmsGenericBlock.md +0 -0
  127. /package/{components → app/components}/public/cms/CmsGenericBlock.vue +0 -0
  128. /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
  129. /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
  130. /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
  131. /package/{components → app/components}/public/cms/CmsPage.md +0 -0
  132. /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
  133. /package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +0 -0
  134. /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
  135. /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
  136. /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
  137. /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
  138. /package/{components/public/cms/element → app/components/public/cms/block}/CmsBlockHtml.vue +0 -0
  139. /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
  140. /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
  141. /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
  142. /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
  143. /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
  144. /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
  145. /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
  146. /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
  147. /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
  148. /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
  149. /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
  150. /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
  151. /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
  152. /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
  153. /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
  154. /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
  155. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
  156. /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
  157. /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
  158. /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
  159. /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
  160. /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
  161. /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
  162. /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
  163. /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
  164. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
  165. /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
  166. /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
  167. /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
  168. /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
  169. /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
  170. /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
  171. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
  172. /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.vue +0 -0
  173. /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
  174. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
  175. /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
  176. /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
  177. /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
  178. /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
  179. /package/{helpers → app/helpers}/html-to-vue/ast.ts +0 -0
  180. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
  181. /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +0 -0
  182. /package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +0 -0
  183. /package/{helpers → app/helpers}/html-to-vue/renderer.ts +0 -0
  184. /package/{helpers → app/helpers}/media/isSpatial.ts +0 -0
@@ -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
+ });
@@ -0,0 +1,107 @@
1
+ import { defineProvider } from "@nuxt/image/runtime";
2
+ import { encodeUrlPath } from "@shopware/helpers";
3
+
4
+ /**
5
+ * Shopware Image Provider for Nuxt Image
6
+ *
7
+ * Generates optimized image URLs compatible with Shopware's remote thumbnail generation feature.
8
+ * This provider appends transformation parameters as query strings to image URLs, which are then
9
+ * processed by your configured image transformation backend.
10
+ *
11
+ * @remarks
12
+ * Shopware has built-in thumbnail generation (using GD2 or ImageMagick) that creates predefined
13
+ * sizes (400x400, 800x800, 1920x1920) during upload. However, for **on-the-fly transformations**
14
+ * via query parameters (like `?width=800`), you need to configure remote thumbnail generation:
15
+ * - Shopware Cloud: Uses Fastly CDN (configured automatically)
16
+ * - Self-hosted: Requires external middleware (Thumbor, Sharp, imgproxy) or plugins like FroshPlatformThumbnailProcessor
17
+ *
18
+ * Without remote thumbnail generation, query parameters will have no effect and original/predefined thumbnails are served
19
+ *
20
+ * @param src - The source image URL (e.g., "/media/image/product.jpg")
21
+ * @param options - Configuration options
22
+ * @param options.modifiers - Image transformation modifiers
23
+ * @param options.modifiers.width - Target width in pixels
24
+ * @param options.modifiers.height - Target height in pixels
25
+ * @param options.modifiers.quality - Image quality (0-100)
26
+ * @param options.modifiers.format - Output format (jpg, png, webp, avif)
27
+ * @param options.modifiers.fit - Resize mode (cover, contain, crop, bounds, crop_center)
28
+ *
29
+ * @returns Object containing the transformed image URL with query parameters
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // Basic usage
34
+ * const result = getImage('/media/image/product.jpg', {
35
+ * modifiers: {
36
+ * width: 800,
37
+ * height: 600,
38
+ * quality: 85,
39
+ * format: 'webp',
40
+ * fit: 'cover'
41
+ * }
42
+ * });
43
+ * // Returns: { url: '/media/image/product.jpg?width=800&height=600&quality=85&format=webp&fit=cover' }
44
+ * ```
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * // With existing query parameters
49
+ * const result = getImage('/media/image/product.jpg?v=123', {
50
+ * modifiers: { width: 400 }
51
+ * });
52
+ * // Returns: { url: '/media/image/product.jpg?v=123&width=400' }
53
+ * ```
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * // No modifiers - returns original URL
58
+ * const result = getImage('/media/image/product.jpg');
59
+ * // Returns: { url: '/media/image/product.jpg' }
60
+ * ```
61
+ *
62
+ * @see {@link https://developer.shopware.com/docs/guides/plugins/plugins/content/media/remote-thumbnail-generation.html | Shopware Remote Thumbnail Generation}
63
+ * @see {@link https://image.nuxt.com/providers/custom | Nuxt Image Custom Providers}
64
+ */
65
+
66
+ export default defineProvider(() => ({
67
+ getImage(src, { modifiers }, _ctx) {
68
+ // Encode special characters in the URL pathname (commas, spaces, etc.)
69
+ const encodedSrc = encodeUrlPath(src);
70
+
71
+ const params = new URLSearchParams();
72
+
73
+ // Map Nuxt Image modifiers to Shopware query parameters
74
+ if (modifiers.width) {
75
+ params.set("width", String(modifiers.width));
76
+ }
77
+
78
+ if (modifiers.height) {
79
+ params.set("height", String(modifiers.height));
80
+ }
81
+
82
+ if (modifiers.quality) {
83
+ params.set("quality", String(modifiers.quality));
84
+ }
85
+
86
+ if (modifiers.format) {
87
+ params.set("format", String(modifiers.format));
88
+ }
89
+
90
+ if (modifiers.fit) {
91
+ params.set("fit", String(modifiers.fit));
92
+ }
93
+
94
+ const query = params.toString();
95
+
96
+ if (!query) {
97
+ return { url: encodedSrc };
98
+ }
99
+
100
+ // Check if URL already has query parameters
101
+ const separator = encodedSrc.includes("?") ? "&" : "?";
102
+
103
+ return {
104
+ url: `${encodedSrc}${separator}${query}`,
105
+ };
106
+ },
107
+ }));
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { NuxtModule } from '@nuxt/schema';
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
- declare const nuxtModule: NuxtModule;
3
+ declare const _default: _nuxt_schema.NuxtModule<_nuxt_schema.ModuleOptions, _nuxt_schema.ModuleOptions, false>;
4
4
 
5
- export { nuxtModule as default };
5
+ export { _default as default };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { NuxtModule } from '@nuxt/schema';
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
- declare const nuxtModule: NuxtModule;
3
+ declare const _default: _nuxt_schema.NuxtModule<_nuxt_schema.ModuleOptions, _nuxt_schema.ModuleOptions, false>;
4
4
 
5
- export { nuxtModule as default };
5
+ export { _default as default };
package/dist/index.mjs CHANGED
@@ -10,7 +10,7 @@ import __cjs_mod__ from 'module';
10
10
  const __filename = __cjs_url__.fileURLToPath(import.meta.url);
11
11
  const __dirname = __cjs_path__.dirname(__filename);
12
12
  const require = __cjs_mod__.createRequire(import.meta.url);
13
- const nuxtModule = defineNuxtModule({
13
+ const index = defineNuxtModule({
14
14
  meta: {
15
15
  name: "@shopware/cms-base",
16
16
  configKey: "shopware-cms"
@@ -28,4 +28,4 @@ const nuxtModule = defineNuxtModule({
28
28
  }
29
29
  });
30
30
 
31
- export { nuxtModule as default };
31
+ export { index as default };
package/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /// <reference types="@nuxt/schema" />
2
+
3
+ export * from "@shopware/composables";
4
+ export * from "./.nuxt/imports";
5
+
6
+ declare module "nuxt/schema" {
7
+ interface AppConfig {
8
+ imagePlaceholder?: {
9
+ color?: string;
10
+ };
11
+ }
12
+ }
package/nuxt.config.ts CHANGED
@@ -1,19 +1,93 @@
1
+ import { createResolver } from "@nuxt/kit";
1
2
  import type { NuxtConfig } from "@nuxt/schema";
2
3
  import { defineNuxtConfig } from "nuxt/config";
4
+
5
+ const { resolve: resolveLayer } = createResolver(import.meta.url);
6
+
3
7
  export default defineNuxtConfig({
8
+ modules: ["@unocss/nuxt", "@nuxt/image"],
9
+
10
+ // @ts-ignore - @nuxt/image config may not be typed in some layer contexts
11
+ image: {
12
+ quality: 90,
13
+ format: ["webp", "avif", "jpg"],
14
+ // Custom Shopware provider that maps Nuxt Image modifiers to Shopware query parameters
15
+ provider: "shopware",
16
+ providers: {
17
+ shopware: {
18
+ name: "shopware",
19
+ provider: resolveLayer("./app/providers/shopware.ts"),
20
+ },
21
+ },
22
+
23
+ // Responsive breakpoints matching UnoCSS
24
+ screens: {
25
+ xs: 320,
26
+ sm: 640,
27
+ md: 768,
28
+ lg: 1024,
29
+ xl: 1280,
30
+ xxl: 1536,
31
+ },
32
+
33
+ // Presets for common CMS use cases
34
+ presets: {
35
+ productCard: {
36
+ modifiers: {
37
+ format: "webp",
38
+ quality: 90,
39
+ fit: "cover",
40
+ },
41
+ },
42
+ productDetail: {
43
+ modifiers: {
44
+ format: "webp",
45
+ quality: 90,
46
+ fit: "contain",
47
+ },
48
+ },
49
+ thumbnail: {
50
+ modifiers: {
51
+ format: "webp",
52
+ quality: 90,
53
+ width: 150,
54
+ height: 150,
55
+ fit: "cover",
56
+ },
57
+ },
58
+ hero: {
59
+ modifiers: {
60
+ format: "webp",
61
+ quality: 95,
62
+ fit: "cover",
63
+ },
64
+ },
65
+ },
66
+ },
67
+
4
68
  components: [
5
69
  {
6
- path: "./components/public",
7
- pathPrefix: false,
8
- // global: true,
70
+ path: resolveLayer("./app/components"),
71
+ pattern: "Sw*",
72
+ extensions: [".vue"],
73
+ global: true,
9
74
  },
10
75
  {
11
- path: "./components/",
12
- pattern: "Sw*",
76
+ path: resolveLayer("./app/components/ui"),
77
+ extensions: [".vue"],
78
+ prefix: "Sw",
79
+ global: true,
80
+ },
81
+ {
82
+ path: resolveLayer("./app/components/public"),
83
+ pathPrefix: false,
84
+ global: true,
13
85
  extensions: [".vue"],
14
- global: false,
15
86
  },
16
87
  ],
88
+ alias: {
89
+ "@cms-assets": resolveLayer("./app/assets"),
90
+ },
17
91
  build: {
18
92
  transpile: ["@shopware/cms-base-layer"],
19
93
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopware/cms-base-layer",
3
- "version": "1.5.0",
3
+ "version": "2.0.0",
4
4
  "description": "Vue CMS Nuxt Layer for Shopware",
5
5
  "author": "Shopware",
6
6
  "repository": {
@@ -23,47 +23,55 @@
23
23
  "main": "./nuxt.config.ts",
24
24
  "files": [
25
25
  "dist",
26
- "components",
26
+ "app",
27
27
  "helpers",
28
28
  "index.cjs",
29
- "nuxt.config.ts"
29
+ "index.d.ts",
30
+ "app.config.ts",
31
+ "nuxt.config.ts",
32
+ "uno.config.ts"
30
33
  ],
31
34
  "dependencies": {
32
- "@nuxt/kit": "3.16.2",
33
- "@tresjs/cientos": "4.3.0",
34
- "@tresjs/core": "4.3.3",
35
+ "@iconify-json/carbon": "1.2.14",
36
+ "@nuxt/image": "2.0.0",
37
+ "@nuxt/kit": "4.2.1",
38
+ "@tresjs/cientos": "4.3.1",
39
+ "@tresjs/core": "4.3.6",
40
+ "@unocss/nuxt": "66.5.4",
35
41
  "@vuelidate/core": "2.0.3",
36
42
  "@vuelidate/validators": "2.0.4",
37
- "@vueuse/core": "13.1.0",
43
+ "@vueuse/core": "14.0.0",
38
44
  "entities": "6.0.0",
39
45
  "html-to-ast": "0.0.6",
40
46
  "three": "0.173.0",
41
- "vue": "3.5.13",
47
+ "unocss": "66.5.4",
48
+ "vue": "3.5.24",
42
49
  "xss": "1.0.15",
43
- "@shopware/composables": "1.9.0",
44
- "@shopware/helpers": "1.4.0",
45
- "@shopware/api-client": "1.3.0"
50
+ "@shopware/composables": "1.10.0",
51
+ "@shopware/api-client": "1.4.0",
52
+ "@shopware/helpers": "1.6.0"
46
53
  },
47
54
  "devDependencies": {
48
55
  "@biomejs/biome": "1.8.3",
49
- "@nuxt/schema": "3.16.2",
56
+ "@nuxt/schema": "4.2.1",
50
57
  "@types/three": "0.173.0",
51
- "@vitest/coverage-v8": "3.1.1",
52
- "nuxt": "3.16.2",
53
- "typescript": "5.8.3",
58
+ "@vitest/coverage-v8": "3.2.4",
59
+ "nuxt": "4.2.1",
60
+ "typescript": "5.9.3",
54
61
  "unbuild": "2.0.0",
55
- "vitest": "3.1.1",
56
- "vue-router": "4.5.0",
57
- "vue-tsc": "2.2.8",
62
+ "vitest": "3.2.4",
63
+ "vue-router": "4.6.3",
64
+ "vue-tsc": "3.1.4",
58
65
  "tsconfig": "0.0.0"
59
66
  },
60
67
  "scripts": {
61
- "build": "unbuild",
68
+ "build": "nuxt prepare && unbuild",
62
69
  "dev": "unbuild --stub",
63
70
  "lint": "biome check .",
64
71
  "lint:fix": "biome check . --write && pnpm run typecheck",
65
- "typecheck": "tsc --noEmit",
72
+ "typecheck": "pnpm nuxt prepare && tsc --noEmit",
66
73
  "test": "vitest run",
67
- "test:watch": "vitest"
74
+ "test:watch": "vitest",
75
+ "check-colors": "tsx scripts/check-unused-colors.ts"
68
76
  }
69
77
  }
package/uno.config.ts ADDED
@@ -0,0 +1,83 @@
1
+ import {
2
+ defineConfig,
3
+ presetAttributify,
4
+ presetIcons,
5
+ presetTypography,
6
+ presetWind3,
7
+ } from "unocss";
8
+
9
+ export default defineConfig({
10
+ shortcuts: {},
11
+ preflights: [
12
+ {
13
+ getCSS: () => `
14
+ /* Filter collapse transition */
15
+ .filter-collapse-enter-active,
16
+ .filter-collapse-leave-active {
17
+ transition: all 0.3s ease-in-out;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .filter-collapse-enter-from,
22
+ .filter-collapse-leave-to {
23
+ max-height: 0;
24
+ opacity: 0;
25
+ }
26
+
27
+ .filter-collapse-enter-to,
28
+ .filter-collapse-leave-from {
29
+ max-height: 1000px;
30
+ opacity: 1;
31
+ }
32
+ `,
33
+ },
34
+ ],
35
+ theme: {
36
+ colors: {
37
+ "brand-primary": "#543B95",
38
+ "surface-surface": "#FFFFFF",
39
+ "outline-outline": "#79747E",
40
+ "outline-outline-variant": "#CAC4D0",
41
+ "outline-outline-primary": "#543B95",
42
+ "surface-on-surface": "#1D1B20",
43
+ "surface-surface-variant": "#FBF6FF",
44
+ "states-success": "#15B31C",
45
+ "surface-on-surface-variant": "#696470",
46
+ "surface-surface-disabled": "#E8E8E8",
47
+ "surface-on-surface-disabled": "#9893A6",
48
+ "surface-surface-primary": "#D0BCFF",
49
+ "states-warning": "#F57C00",
50
+ "surface-surface-container": "#F3EDF7",
51
+ "surface-surface-container-highest": "#E6E0E9",
52
+ "states-error": "#D12D24",
53
+ "states-on-error": "#FFFFFF",
54
+ "brand-primary-hover": "#45317A",
55
+ "brand-on-primary": "#FFFFFF",
56
+ "brand-secondary": "#E1D5FF",
57
+ "brand-secondary-hover": "#D0BCFC",
58
+ "brand-on-secondary": "#3A276A",
59
+ "brand-tertiary": "#F1F1F1",
60
+ "brand-tertiary-hover": "#E3E3E3",
61
+ "brand-on-tertiary": "#1D1B20",
62
+ "other-sale": "#D12D24",
63
+ },
64
+ fontFamily: {
65
+ inter: "Inter",
66
+ Noto_Serif: "Noto Serif",
67
+ },
68
+ },
69
+ safelist: ["states-success", "states-error", "states-info", "states-warning"],
70
+ presets: [
71
+ presetWind3(),
72
+ presetIcons({
73
+ collections: {
74
+ carbon: () =>
75
+ import("@iconify-json/carbon/icons.json", {
76
+ with: { type: "json" },
77
+ }).then((i) => i.default),
78
+ },
79
+ }),
80
+ presetAttributify(),
81
+ presetTypography(),
82
+ ],
83
+ });
@@ -1,44 +0,0 @@
1
- <script setup lang="ts">
2
- import type { Schemas } from "#shopware";
3
-
4
- const props = withDefaults(
5
- defineProps<{
6
- activeCategory: Schemas["Category"];
7
- elements: Schemas["Category"][];
8
- level: number;
9
- }>(),
10
- {
11
- level: 0,
12
- },
13
- );
14
-
15
- function getHighlightCategory(navigationElement: Schemas["Category"]) {
16
- return (
17
- (props.activeCategory?.path || "").includes(navigationElement.id) ||
18
- navigationElement.id === props.activeCategory?.id
19
- );
20
- }
21
- </script>
22
- <template>
23
- <ul v-if="props.elements?.length" class="list-none m-0 px-5">
24
- <li
25
- v-for="(navigationElement, index) in props.elements"
26
- :key="index"
27
- :class="{
28
- 'border-b border-gray-200': props.level === 0,
29
- }"
30
- >
31
- <SwCategoryNavigationLink
32
- :navigation-element="navigationElement"
33
- :is-highlighted="getHighlightCategory(navigationElement)"
34
- :is-active="navigationElement.id === props.activeCategory?.id"
35
- />
36
- <SwCategoryNavigation
37
- v-if="navigationElement.children"
38
- :elements="navigationElement.children"
39
- :active-category="props.activeCategory"
40
- :level="props.level + 1"
41
- />
42
- </li>
43
- </ul>
44
- </template>