@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.
- package/README.md +328 -13
- package/app/app.config.ts +7 -0
- package/app/assets/icons/check-circle.svg +3 -0
- package/app/assets/icons/checkmark.svg +3 -0
- package/app/assets/icons/chevron.svg +3 -0
- package/app/assets/icons/exclamation-circle.svg +3 -0
- package/app/assets/icons/star-empty.svg +3 -0
- package/app/assets/icons/star-filled.svg +3 -0
- package/app/assets/icons/user.svg +1 -0
- package/app/components/SwCategoryNavigation.vue +76 -0
- package/app/components/SwCategoryNavigationLink.vue +128 -0
- package/{components → app/components}/SwContactForm.vue +27 -27
- package/app/components/SwFilterChips.vue +144 -0
- package/app/components/SwListingProductPrice.vue +89 -0
- package/{components → app/components}/SwNewsletterForm.vue +45 -34
- package/{components → app/components}/SwPagination.vue +3 -5
- package/{components → app/components}/SwProductAddToCart.vue +22 -27
- package/app/components/SwProductCard.vue +170 -0
- package/app/components/SwProductCardDetails.vue +57 -0
- package/app/components/SwProductCardImage.vue +87 -0
- package/app/components/SwProductCardSkeleton.vue +33 -0
- package/app/components/SwProductListingFilter.vue +64 -0
- package/app/components/SwProductListingFilters.vue +308 -0
- package/{components → app/components}/SwProductReviews.vue +28 -13
- package/app/components/SwProductReviewsForm.vue +292 -0
- package/app/components/SwQuantitySelect.vue +106 -0
- package/{components → app/components}/SwSlider.vue +4 -4
- package/app/components/SwSortDropdown.vue +83 -0
- package/app/components/SwStockInfo.vue +44 -0
- package/{components → app/components}/SwVariantConfigurator.vue +1 -1
- package/app/components/listing-filters/SwFilterPrice.vue +214 -0
- package/app/components/listing-filters/SwFilterProperties.vue +113 -0
- package/app/components/listing-filters/SwFilterRating.vue +90 -0
- package/app/components/listing-filters/SwFilterShippingFree.vue +107 -0
- package/{components → app/components}/public/cms/CmsPage.vue +19 -4
- package/{components → app/components}/public/cms/block/CmsBlockGalleryBuybox.vue +5 -5
- package/{components → app/components}/public/cms/block/CmsBlockImageBubbleRow.vue +5 -5
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +41 -0
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +42 -0
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +37 -0
- package/{components → app/components}/public/cms/block/CmsBlockImageSimpleGrid.vue +11 -5
- package/{components → app/components}/public/cms/block/CmsBlockImageText.vue +7 -3
- package/{components → app/components}/public/cms/block/CmsBlockImageTextBubble.vue +13 -16
- package/{components → app/components}/public/cms/block/CmsBlockImageTextCover.vue +7 -9
- package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +88 -0
- package/app/components/public/cms/block/CmsBlockImageTextRow.vue +53 -0
- package/{components → app/components}/public/cms/block/CmsBlockImageThreeColumn.vue +10 -4
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +37 -0
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +37 -0
- package/{components → app/components}/public/cms/block/CmsBlockProductHeading.vue +1 -1
- package/{components → app/components}/public/cms/block/CmsBlockProductThreeColumn.vue +10 -4
- package/{components → app/components}/public/cms/block/CmsBlockSidebarFilter.vue +3 -1
- package/app/components/public/cms/block/CmsBlockTextOnImage.vue +30 -0
- package/{components → app/components}/public/cms/block/CmsBlockTextTeaserSection.vue +4 -4
- package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.vue +3 -5
- package/app/components/public/cms/element/CmsElementBuyBox.vue +145 -0
- package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +53 -0
- package/{components → app/components}/public/cms/element/CmsElementCrossSelling.vue +3 -3
- package/{components → app/components}/public/cms/element/CmsElementImage.vue +52 -13
- package/app/components/public/cms/element/CmsElementImageGallery.vue +158 -0
- package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
- package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +2 -1
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
- package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +23 -94
- package/app/components/public/cms/element/CmsElementProductName.vue +11 -0
- package/{components → app/components}/public/cms/element/CmsElementProductSlider.vue +4 -4
- package/{components → app/components}/public/cms/element/CmsElementText.vue +8 -2
- package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.vue +8 -2
- package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
- package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +1 -1
- package/app/components/public/cms/section/CmsSectionSidebar.vue +36 -0
- package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
- package/app/components/ui/BaseButton.vue +99 -0
- package/app/components/ui/BaseIcon.vue +15 -0
- package/app/components/ui/Checkbox.vue +49 -0
- package/app/components/ui/CheckmarkIcon.vue +23 -0
- package/app/components/ui/ChevronIcon.vue +37 -0
- package/app/components/ui/ExclamationIcon.vue +11 -0
- package/app/components/ui/IconButton.vue +32 -0
- package/app/components/ui/RadioButton.vue +26 -0
- package/app/components/ui/StarIcon.vue +18 -0
- package/app/components/ui/SwitchButton.vue +100 -0
- package/app/components/ui/UserIcon.vue +11 -0
- package/app/components/ui/WishlistIcon.vue +20 -0
- package/app/composables/useImagePlaceholder.ts +27 -0
- package/{helpers → app/helpers}/clientOnly.ts +5 -0
- package/app/providers/shopware.test.ts +213 -0
- package/app/providers/shopware.ts +107 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +2 -2
- package/index.d.ts +12 -0
- package/nuxt.config.ts +80 -6
- package/package.json +29 -21
- package/uno.config.ts +83 -0
- package/components/SwCategoryNavigation.vue +0 -44
- package/components/SwCategoryNavigationLink.vue +0 -57
- package/components/SwListingProductPrice.vue +0 -89
- package/components/SwProductCard.vue +0 -286
- package/components/SwProductListingFilter.vue +0 -42
- package/components/SwProductListingFilters.vue +0 -292
- package/components/listing-filters/SwFilterPrice.vue +0 -160
- package/components/listing-filters/SwFilterProperties.vue +0 -123
- package/components/listing-filters/SwFilterRating.vue +0 -101
- package/components/listing-filters/SwFilterShippingFree.vue +0 -104
- package/components/public/cms/block/CmsBlockImageFourColumn.vue +0 -29
- package/components/public/cms/block/CmsBlockImageHighlightRow.vue +0 -27
- package/components/public/cms/block/CmsBlockImageTextGallery.vue +0 -85
- package/components/public/cms/block/CmsBlockImageTextRow.vue +0 -43
- package/components/public/cms/block/CmsBlockImageThreeCover.vue +0 -27
- package/components/public/cms/block/CmsBlockImageTwoColumn.vue +0 -25
- package/components/public/cms/block/CmsBlockTextOnImage.vue +0 -20
- package/components/public/cms/element/CmsBlockHtml.md +0 -1
- package/components/public/cms/element/CmsElementBuyBox.vue +0 -190
- package/components/public/cms/element/CmsElementCategoryNavigation.vue +0 -167
- package/components/public/cms/element/CmsElementImageGallery.vue +0 -249
- package/components/public/cms/element/CmsElementProductDescriptionReviews.vue +0 -123
- package/components/public/cms/element/CmsElementProductName.vue +0 -10
- package/components/public/cms/section/CmsSectionSidebar.vue +0 -49
- package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
- /package/{components → app/components}/SwMedia3D.vue +0 -0
- /package/{components → app/components}/SwProductGallery.vue +0 -0
- /package/{components → app/components}/SwProductPrice.vue +0 -0
- /package/{components → app/components}/SwProductUnits.vue +0 -0
- /package/{components → app/components}/SwSharedPrice.vue +0 -0
- /package/{components → app/components}/public/cms/CmsGenericBlock.md +0 -0
- /package/{components → app/components}/public/cms/CmsGenericBlock.vue +0 -0
- /package/{components → app/components}/public/cms/CmsGenericElement.md +0 -0
- /package/{components → app/components}/public/cms/CmsGenericElement.vue +0 -0
- /package/{components → app/components}/public/cms/CmsNoComponent.vue +0 -0
- /package/{components → app/components}/public/cms/CmsPage.md +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCategoryNavigation.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCrossSelling.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockCustomForm.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockDefault.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockForm.vue +0 -0
- /package/{components/public/cms/element → app/components/public/cms/block}/CmsBlockHtml.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImage.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageCover.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageGallery.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockImageSlider.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductDescriptionReviews.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductListing.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockProductSlider.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockText.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextHero.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextTeaser.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockVimeoVideo.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockYoutubeVideo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementBuyBox.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCategoryNavigation.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCrossSelling.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCustomForm.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementCustomForm.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementForm.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementForm.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementHtml.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImage.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageGallery.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageGallery3dPlaceholder.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementImageSlider.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementManufacturerLogo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductBox.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductDescriptionReviews.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductListing.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductName.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementProductSlider.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementSidebarFilter.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementText.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.md +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementVimeoVideo.vue +0 -0
- /package/{components → app/components}/public/cms/element/CmsElementYoutubeVideo.md +0 -0
- /package/{components → app/components}/public/cms/section/CmsSectionDefault.md +0 -0
- /package/{components → app/components}/public/cms/section/CmsSectionSidebar.md +0 -0
- /package/{helpers → app/helpers}/html-to-vue/ast.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.test.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +0 -0
- /package/{helpers → app/helpers}/html-to-vue/renderer.ts +0 -0
- /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
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
|
|
3
|
-
declare const
|
|
3
|
+
declare const _default: _nuxt_schema.NuxtModule<_nuxt_schema.ModuleOptions, _nuxt_schema.ModuleOptions, false>;
|
|
4
4
|
|
|
5
|
-
export {
|
|
5
|
+
export { _default as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
|
|
3
|
-
declare const
|
|
3
|
+
declare const _default: _nuxt_schema.NuxtModule<_nuxt_schema.ModuleOptions, _nuxt_schema.ModuleOptions, false>;
|
|
4
4
|
|
|
5
|
-
export {
|
|
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
|
|
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 {
|
|
31
|
+
export { index as default };
|
package/index.d.ts
ADDED
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
|
|
7
|
-
|
|
8
|
-
|
|
70
|
+
path: resolveLayer("./app/components"),
|
|
71
|
+
pattern: "Sw*",
|
|
72
|
+
extensions: [".vue"],
|
|
73
|
+
global: true,
|
|
9
74
|
},
|
|
10
75
|
{
|
|
11
|
-
path: "./components/",
|
|
12
|
-
|
|
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": "
|
|
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
|
-
"
|
|
26
|
+
"app",
|
|
27
27
|
"helpers",
|
|
28
28
|
"index.cjs",
|
|
29
|
-
"
|
|
29
|
+
"index.d.ts",
|
|
30
|
+
"app.config.ts",
|
|
31
|
+
"nuxt.config.ts",
|
|
32
|
+
"uno.config.ts"
|
|
30
33
|
],
|
|
31
34
|
"dependencies": {
|
|
32
|
-
"@
|
|
33
|
-
"@
|
|
34
|
-
"@
|
|
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": "
|
|
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
|
-
"
|
|
47
|
+
"unocss": "66.5.4",
|
|
48
|
+
"vue": "3.5.24",
|
|
42
49
|
"xss": "1.0.15",
|
|
43
|
-
"@shopware/composables": "1.
|
|
44
|
-
"@shopware/
|
|
45
|
-
"@shopware/
|
|
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": "
|
|
56
|
+
"@nuxt/schema": "4.2.1",
|
|
50
57
|
"@types/three": "0.173.0",
|
|
51
|
-
"@vitest/coverage-v8": "3.
|
|
52
|
-
"nuxt": "
|
|
53
|
-
"typescript": "5.
|
|
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.
|
|
56
|
-
"vue-router": "4.
|
|
57
|
-
"vue-tsc": "
|
|
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>
|