@shopware/cms-base-layer 1.5.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +398 -12
- package/app/app.config.ts +18 -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 +83 -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/SwFilterDropdown.vue +54 -0
- package/app/components/SwListingProductPrice.vue +89 -0
- package/{components → app/components}/SwMedia3D.vue +4 -2
- 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 +169 -0
- package/app/components/SwProductCardDetails.vue +74 -0
- package/app/components/SwProductCardImage.vue +90 -0
- package/app/components/SwProductCardSkeleton.vue +33 -0
- package/app/components/SwProductGallery.vue +43 -0
- package/app/components/SwProductListingFilter.vue +75 -0
- package/app/components/SwProductListingFilters.vue +304 -0
- package/app/components/SwProductListingFiltersHorizontal.vue +306 -0
- package/{components → app/components}/SwProductPrice.vue +3 -3
- package/app/components/SwProductRating.vue +40 -0
- package/{components → app/components}/SwProductReviews.vue +25 -23
- package/app/components/SwProductReviewsForm.vue +292 -0
- package/{components → app/components}/SwProductUnits.vue +10 -15
- package/app/components/SwQuantitySelect.vue +103 -0
- package/{components → app/components}/SwSlider.vue +154 -55
- package/app/components/SwSortDropdown.vue +87 -0
- package/app/components/SwStockInfo.vue +44 -0
- package/{components → app/components}/SwVariantConfigurator.vue +13 -12
- package/app/components/listing-filters/SwFilterPrice.vue +219 -0
- package/app/components/listing-filters/SwFilterProperties.vue +120 -0
- package/app/components/listing-filters/SwFilterRating.vue +99 -0
- package/app/components/listing-filters/SwFilterShippingFree.vue +114 -0
- package/app/components/public/cms/CmsBlockSpatialViewer.vue +94 -0
- package/app/components/public/cms/CmsGenericBlock.md +42 -0
- package/{components → app/components}/public/cms/CmsGenericBlock.vue +15 -1
- package/{components → app/components}/public/cms/CmsPage.md +19 -2
- package/{components → app/components}/public/cms/CmsPage.vue +30 -5
- package/{components → app/components}/public/cms/block/CmsBlockCenterText.vue +1 -1
- 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/{components → app/components}/public/cms/block/CmsBlockTextOnImage.vue +8 -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 +22 -6
- package/{components → app/components}/public/cms/element/CmsElementImage.vue +58 -21
- package/app/components/public/cms/element/CmsElementImageGallery.vue +225 -0
- package/{components → app/components}/public/cms/element/CmsElementImageSlider.vue +2 -2
- package/{components → app/components}/public/cms/element/CmsElementProductBox.vue +8 -1
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +217 -0
- package/{components → app/components}/public/cms/element/CmsElementProductListing.vue +31 -95
- package/app/components/public/cms/element/CmsElementProductName.vue +16 -0
- package/app/components/public/cms/element/CmsElementProductSlider.vue +101 -0
- package/app/components/public/cms/element/CmsElementSidebarFilter.vue +20 -0
- package/{components → app/components}/public/cms/element/CmsElementText.vue +17 -12
- package/app/components/public/cms/element/SwProductListingPagination.vue +70 -0
- package/{components → app/components}/public/cms/section/CmsSectionDefault.vue +2 -2
- package/app/components/public/cms/section/CmsSectionSidebar.vue +39 -0
- package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +28 -0
- package/app/components/ui/BaseButton.vue +102 -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 +34 -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 +15 -0
- package/app/composables/useImagePlaceholder.ts +27 -0
- package/app/composables/useLcpImagePreload.test.ts +229 -0
- package/app/composables/useLcpImagePreload.ts +39 -0
- package/{helpers → app/helpers}/clientOnly.ts +5 -0
- package/app/helpers/cms/findFirstCmsImageUrl.ts +86 -0
- package/app/helpers/cms/getImageSizes.test.ts +50 -0
- package/app/helpers/cms/getImageSizes.ts +36 -0
- package/app/helpers/html-to-vue/ast.ts +106 -0
- package/{helpers → app/helpers}/html-to-vue/getOptionsFromNode.ts +1 -1
- package/{helpers → app/helpers}/html-to-vue/renderToHtml.ts +7 -11
- package/app/helpers/html-to-vue/renderer.ts +116 -0
- package/app/plugins/unocss-runtime.client.ts +23 -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 +36 -0
- package/nuxt.config.ts +100 -6
- package/package.json +33 -23
- package/uno.config.ts +94 -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/SwProductGallery.vue +0 -39
- 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/CmsGenericBlock.md +0 -27
- 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/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/element/CmsElementProductSlider.vue +0 -80
- package/components/public/cms/element/CmsElementSidebarFilter.vue +0 -12
- package/components/public/cms/section/CmsSectionSidebar.vue +0 -41
- package/components/public/cms/skeleton/ProductCardSkeleton.vue +0 -44
- package/helpers/html-to-vue/ast.ts +0 -72
- package/helpers/html-to-vue/renderer.ts +0 -56
- /package/{components → app/components}/SwSharedPrice.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/block/CmsBlockCategoryNavigation.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 → 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/CmsBlockTextTeaserSection.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextThreeColumn.vue +0 -0
- /package/{components → app/components}/public/cms/block/CmsBlockTextTwoColumn.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/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/element/CmsElementYoutubeVideo.vue +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/getOptionsFromNode.test.ts +0 -0
- /package/{helpers → app/helpers}/media/isSpatial.ts +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Based on the https://github.com/HCESrl/html-to-vue
|
|
3
|
+
*/
|
|
4
|
+
// @ts-expect-error - html-to-ast has type definition issues with package.json exports
|
|
5
|
+
import { parse } from "html-to-ast";
|
|
6
|
+
import type { NodeObject } from "./getOptionsFromNode";
|
|
7
|
+
|
|
8
|
+
type ASTNode = NodeObject | NodeObject[];
|
|
9
|
+
|
|
10
|
+
type VisitCallback = (
|
|
11
|
+
node: unknown,
|
|
12
|
+
parent: NodeObject | null,
|
|
13
|
+
key: string | null,
|
|
14
|
+
index: number | undefined,
|
|
15
|
+
) => void;
|
|
16
|
+
|
|
17
|
+
type ExtraComponentConfig = {
|
|
18
|
+
conditions: (node: NodeObject) => boolean;
|
|
19
|
+
renderer?: unknown;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type RectifyConfig = {
|
|
23
|
+
extraComponentsMap?: Record<string, ExtraComponentConfig>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Visit each node in the AST - with callback (adapted from https://lihautan.com/manipulating-ast-with-javascript/)
|
|
28
|
+
* @param ast html-parse-stringify AST
|
|
29
|
+
* @param callback
|
|
30
|
+
*/
|
|
31
|
+
function _visitAST(ast: ASTNode, callback: VisitCallback): void {
|
|
32
|
+
function _visit(
|
|
33
|
+
node: unknown,
|
|
34
|
+
parent: NodeObject | null,
|
|
35
|
+
key: string | null,
|
|
36
|
+
index: number | undefined,
|
|
37
|
+
): void {
|
|
38
|
+
callback(node, parent, key, index);
|
|
39
|
+
if (Array.isArray(node)) {
|
|
40
|
+
// node is an array
|
|
41
|
+
node.forEach((value, idx) => {
|
|
42
|
+
_visit(value, node as unknown as NodeObject, null, idx);
|
|
43
|
+
});
|
|
44
|
+
} else if (isNode(node)) {
|
|
45
|
+
const keys = Object.keys(node);
|
|
46
|
+
for (let i = 0; i < keys.length; i++) {
|
|
47
|
+
const childKey = keys[i];
|
|
48
|
+
if (childKey === undefined) continue;
|
|
49
|
+
const child = (node as Record<string, unknown>)[childKey];
|
|
50
|
+
if (Array.isArray(child)) {
|
|
51
|
+
for (let j = 0; j < child.length; j++) {
|
|
52
|
+
_visit(child[j], node, key, j);
|
|
53
|
+
}
|
|
54
|
+
} else if (isNode(child)) {
|
|
55
|
+
_visit(child, node, key, undefined);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
_visit(ast, null, null, undefined);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
*
|
|
65
|
+
* @param node html-parse-stringify AST node
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
export function isNode(node: unknown): node is NodeObject {
|
|
69
|
+
return (
|
|
70
|
+
typeof node === "object" &&
|
|
71
|
+
node !== null &&
|
|
72
|
+
typeof (node as NodeObject).type !== "undefined"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function generateAST(html: string): ASTNode {
|
|
77
|
+
return parse(html) as ASTNode;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Converts ast html nodes in vue components
|
|
82
|
+
* @param ast
|
|
83
|
+
* @param config
|
|
84
|
+
* @returns {*}
|
|
85
|
+
*/
|
|
86
|
+
export function rectifyAST(ast: ASTNode, config: RectifyConfig): ASTNode {
|
|
87
|
+
const _ast = JSON.parse(JSON.stringify(ast)) as ASTNode;
|
|
88
|
+
const keys = config.extraComponentsMap
|
|
89
|
+
? Object.keys(config.extraComponentsMap)
|
|
90
|
+
: [];
|
|
91
|
+
_visitAST(_ast, (node) => {
|
|
92
|
+
if (!isNode(node)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// checking whether the AST has some components that has to become Vue Components
|
|
96
|
+
for (let i = 0; i < keys.length; i++) {
|
|
97
|
+
const currentKey = keys[i];
|
|
98
|
+
if (currentKey === undefined) continue;
|
|
99
|
+
const componentConfig = config.extraComponentsMap?.[currentKey];
|
|
100
|
+
if (componentConfig?.conditions(node)) {
|
|
101
|
+
node.name = currentKey;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return _ast;
|
|
106
|
+
}
|
|
@@ -3,17 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ComponentInternalInstance, h } from "vue";
|
|
6
|
-
import { generateAST, rectifyAST } from "./ast";
|
|
7
|
-
import { renderer } from "./renderer";
|
|
6
|
+
import { type RectifyConfig, generateAST, rectifyAST } from "./ast";
|
|
7
|
+
import { type RendererConfig, renderer } from "./renderer";
|
|
8
8
|
|
|
9
|
-
type DefaultConfig =
|
|
10
|
-
container: {
|
|
11
|
-
type: string;
|
|
12
|
-
};
|
|
13
|
-
extraComponentsMap: Record<string, unknown>;
|
|
14
|
-
renderAnyway: boolean;
|
|
15
|
-
textTransformer: (text: string) => string;
|
|
16
|
-
};
|
|
9
|
+
type DefaultConfig = RendererConfig;
|
|
17
10
|
|
|
18
11
|
const defaultConfig: DefaultConfig = {
|
|
19
12
|
container: {
|
|
@@ -33,7 +26,10 @@ export function renderHtml(
|
|
|
33
26
|
) {
|
|
34
27
|
const mergedConfig = Object.assign(defaultConfig, config);
|
|
35
28
|
const _ast = generateAST(html);
|
|
36
|
-
const
|
|
29
|
+
const rectifyConfig: RectifyConfig = {
|
|
30
|
+
extraComponentsMap: config.extraComponentsMap,
|
|
31
|
+
};
|
|
32
|
+
const _rectifiedAst = rectifyAST(_ast, rectifyConfig);
|
|
37
33
|
|
|
38
34
|
return renderer(
|
|
39
35
|
_rectifiedAst,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Based on the https://github.com/HCESrl/html-to-vue
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ComponentInternalInstance, VNode } from "vue";
|
|
6
|
+
import { isNode } from "./ast";
|
|
7
|
+
import type { NodeObject } from "./getOptionsFromNode";
|
|
8
|
+
import { getOptionsFromNode } from "./getOptionsFromNode";
|
|
9
|
+
|
|
10
|
+
type ASTNode = NodeObject | NodeObject[];
|
|
11
|
+
|
|
12
|
+
type RawChildren = string | number | boolean | VNode;
|
|
13
|
+
|
|
14
|
+
type ExtraComponentRenderer = (
|
|
15
|
+
node: NodeObject,
|
|
16
|
+
children: RawChildren[],
|
|
17
|
+
createElement: typeof import("vue").h,
|
|
18
|
+
context: ComponentInternalInstance | null,
|
|
19
|
+
) => VNode;
|
|
20
|
+
|
|
21
|
+
export type ExtraComponentConfig = {
|
|
22
|
+
conditions: (node: NodeObject) => boolean;
|
|
23
|
+
renderer: ExtraComponentRenderer;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type RendererConfig = {
|
|
27
|
+
container: {
|
|
28
|
+
type: string;
|
|
29
|
+
};
|
|
30
|
+
extraComponentsMap: Record<string, ExtraComponentConfig>;
|
|
31
|
+
renderAnyway: boolean;
|
|
32
|
+
textTransformer: (text: string) => string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function flattenChildren(
|
|
36
|
+
children: RawChildren | RawChildren[] | undefined,
|
|
37
|
+
): RawChildren[] {
|
|
38
|
+
if (children === undefined) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(children)) {
|
|
42
|
+
const result: RawChildren[] = [];
|
|
43
|
+
for (const child of children) {
|
|
44
|
+
if (child !== null && child !== undefined) {
|
|
45
|
+
if (Array.isArray(child)) {
|
|
46
|
+
result.push(...flattenChildren(child));
|
|
47
|
+
} else {
|
|
48
|
+
result.push(child);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
return [children];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* rendering the ast into vue render functions
|
|
59
|
+
* @param ast AST generated by html-parse-stringify
|
|
60
|
+
* @param config our configuration
|
|
61
|
+
* @param createElement vue's createElement
|
|
62
|
+
* @param context vue functional component context
|
|
63
|
+
* @param resolveUrl function to resolve URLs
|
|
64
|
+
*/
|
|
65
|
+
export function renderer(
|
|
66
|
+
ast: ASTNode,
|
|
67
|
+
config: RendererConfig,
|
|
68
|
+
createElement: typeof import("vue").h,
|
|
69
|
+
context: ComponentInternalInstance | null,
|
|
70
|
+
resolveUrl: (url: string) => string,
|
|
71
|
+
): VNode {
|
|
72
|
+
function _render(
|
|
73
|
+
h: typeof createElement,
|
|
74
|
+
node: unknown,
|
|
75
|
+
): RawChildren | RawChildren[] | undefined {
|
|
76
|
+
if (Array.isArray(node)) {
|
|
77
|
+
const nodes: RawChildren[] = [];
|
|
78
|
+
// node is an array
|
|
79
|
+
for (const subnode of node) {
|
|
80
|
+
const rendered = _render(h, subnode);
|
|
81
|
+
const flattened = flattenChildren(rendered);
|
|
82
|
+
nodes.push(...flattened);
|
|
83
|
+
}
|
|
84
|
+
return nodes;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (isNode(node)) {
|
|
88
|
+
// node is either a node with children or a node or a text node
|
|
89
|
+
if (node.type === "text") {
|
|
90
|
+
return config.textTransformer(node.content); // return text
|
|
91
|
+
}
|
|
92
|
+
if (node.type === "tag") {
|
|
93
|
+
const transformedNode = getOptionsFromNode(node, resolveUrl);
|
|
94
|
+
const children: RawChildren[] = [];
|
|
95
|
+
for (const child of node.children) {
|
|
96
|
+
const rendered = _render(h, child);
|
|
97
|
+
const flattened = flattenChildren(rendered);
|
|
98
|
+
children.push(...flattened);
|
|
99
|
+
}
|
|
100
|
+
// if it's an extra component use custom renderer
|
|
101
|
+
const componentConfig = config.extraComponentsMap[node.name];
|
|
102
|
+
if (componentConfig !== undefined) {
|
|
103
|
+
return componentConfig.renderer(node, children, h, context);
|
|
104
|
+
}
|
|
105
|
+
// else, create normal html element
|
|
106
|
+
return h(node.name, transformedNode, [...children]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const rendered = _render(createElement, ast);
|
|
113
|
+
const children = flattenChildren(rendered);
|
|
114
|
+
|
|
115
|
+
return createElement(config.container.type, context?.data || {}, children);
|
|
116
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import initUnocssRuntime from "@unocss/runtime";
|
|
2
|
+
import { presetWind3 } from "unocss";
|
|
3
|
+
import { defineNuxtPlugin, useAppConfig } from "#imports";
|
|
4
|
+
import unoConfig from "../../uno.config";
|
|
5
|
+
|
|
6
|
+
// Resolves UnoCSS utility classes at runtime via DOM MutationObserver.
|
|
7
|
+
// Needed when CMS content contains dynamic classes not known at build time,
|
|
8
|
+
// since extracting all CMS-used classes from the backend during build is impractical.
|
|
9
|
+
// Trade-off: may cause layout shift as styles are applied after hydration.
|
|
10
|
+
// Can be disabled via app.config.ts: { unocssRuntime: false }
|
|
11
|
+
// When disabled, CMS classes unknown at build time won't be resolved —
|
|
12
|
+
// add them to the UnoCSS safelist in uno.config.ts to ensure they are generated.
|
|
13
|
+
export default defineNuxtPlugin(() => {
|
|
14
|
+
const appConfig = useAppConfig();
|
|
15
|
+
if (!appConfig.unocssRuntime) return;
|
|
16
|
+
|
|
17
|
+
initUnocssRuntime({
|
|
18
|
+
defaults: {
|
|
19
|
+
theme: unoConfig.theme,
|
|
20
|
+
presets: [presetWind3()],
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -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
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
/** Placeholder shown while CMS images are loading */
|
|
9
|
+
imagePlaceholder?: {
|
|
10
|
+
/** CSS color value for the placeholder background */
|
|
11
|
+
color?: string;
|
|
12
|
+
};
|
|
13
|
+
/** CDN optimization options applied to CMS background images and synthetic srcsets */
|
|
14
|
+
backgroundImage?: {
|
|
15
|
+
/** Output format passed to the CDN (e.g. "webp", "avif") */
|
|
16
|
+
format?: string;
|
|
17
|
+
/** Image quality 1-100 passed to the CDN */
|
|
18
|
+
quality?: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Maps CMS block slot count to a responsive `sizes` attribute value.
|
|
22
|
+
* Used by CmsGenericBlock to provide sizing hints to child image elements.
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* imageSizes: {
|
|
26
|
+
* 1: "(max-width: 768px) 100vw, 100vw",
|
|
27
|
+
* 2: "(max-width: 768px) 100vw, 50vw",
|
|
28
|
+
* default: "(max-width: 768px) 50vw, 25vw",
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
imageSizes?: Record<string | number, string>;
|
|
33
|
+
/** Enable UnoCSS runtime for dynamic class generation */
|
|
34
|
+
unocssRuntime?: boolean;
|
|
35
|
+
}
|
|
36
|
+
}
|