@shoppexio/builder-runtime 0.1.1 → 0.1.2
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/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/layout.d.ts +3 -8
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +2 -10
- package/dist/preview-fixtures.d.ts +16 -0
- package/dist/preview-fixtures.d.ts.map +1 -0
- package/dist/preview-fixtures.js +40 -0
- package/dist/product-page.d.ts +13 -0
- package/dist/product-page.d.ts.map +1 -0
- package/dist/product-page.js +18 -0
- package/dist/react.d.ts +31 -2
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +122 -42
- package/dist/search-bar-settings.d.ts +33 -0
- package/dist/search-bar-settings.d.ts.map +1 -0
- package/dist/search-bar-settings.js +99 -0
- package/dist/standard-product-blocks.d.ts +48 -0
- package/dist/standard-product-blocks.d.ts.map +1 -0
- package/dist/standard-product-blocks.js +45 -0
- package/dist/standard-product-page.d.ts +69 -0
- package/dist/standard-product-page.d.ts.map +1 -0
- package/dist/standard-product-page.js +89 -0
- package/dist/storefront-google-fonts.d.ts +2 -0
- package/dist/storefront-google-fonts.d.ts.map +1 -0
- package/dist/storefront-google-fonts.js +28 -0
- package/package.json +3 -3
- package/src/builder-runtime.test.ts +33 -0
- package/src/index.ts +4 -0
- package/src/layout.ts +11 -21
- package/src/preview-fixtures.ts +56 -0
- package/src/product-page.test.ts +37 -0
- package/src/product-page.ts +32 -0
- package/src/react-runtime.test.tsx +42 -0
- package/src/react.tsx +214 -45
- package/src/search-bar-settings.test.ts +72 -0
- package/src/search-bar-settings.ts +176 -0
- package/src/standard-product-blocks.test.tsx +93 -0
- package/src/standard-product-blocks.tsx +121 -0
- package/src/standard-product-page.test.ts +171 -0
- package/src/standard-product-page.ts +169 -0
- package/src/storefront-google-fonts.test.ts +31 -0
- package/src/storefront-google-fonts.ts +43 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { BlockInstance } from '@shoppex/builder-contracts';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { getProductPageBlockAttributes } from './product-page.js';
|
|
4
|
+
import { type StandardBuyBoxLabels, type StandardDetailsLabels, type StandardProductTabSpec } from './standard-product-page.js';
|
|
5
|
+
import { type BuilderBlockRegistry } from './react.js';
|
|
6
|
+
export { STANDARD_PRODUCT_BLOCK_TYPES, STANDARD_PRODUCT_PRIMARY_BLOCK_TYPES, STANDARD_PRODUCT_SECONDARY_BLOCK_TYPES, splitStandardProductPageBlocks, buildStandardProductInfoTabs, resolveStandardBuyBoxLabels, resolveStandardDetailsLabels, resolveStandardRelatedProductsTitle, resolveScopedBlockSettingText, } from './standard-product-page.js';
|
|
7
|
+
export type { StandardBuyBoxLabels, StandardDetailsLabels, StandardProductTabSpec, StandardProductBlockType, StandardProductSettingScope, } from './standard-product-page.js';
|
|
8
|
+
type StandardProductBlockAttrs = ReturnType<typeof getProductPageBlockAttributes>;
|
|
9
|
+
export type StandardProductBlockRegistrySlots = {
|
|
10
|
+
renderGallery: (input: {
|
|
11
|
+
block: BlockInstance;
|
|
12
|
+
attrs: StandardProductBlockAttrs;
|
|
13
|
+
}) => ReactNode;
|
|
14
|
+
renderBuyBox: (input: {
|
|
15
|
+
block: BlockInstance;
|
|
16
|
+
attrs: StandardProductBlockAttrs;
|
|
17
|
+
labels: StandardBuyBoxLabels;
|
|
18
|
+
}) => ReactNode;
|
|
19
|
+
renderDetails: (input: {
|
|
20
|
+
block: BlockInstance;
|
|
21
|
+
attrs: StandardProductBlockAttrs;
|
|
22
|
+
labels: StandardDetailsLabels;
|
|
23
|
+
tabs: StandardProductTabSpec[];
|
|
24
|
+
}) => ReactNode;
|
|
25
|
+
renderRelatedProducts: (input: {
|
|
26
|
+
block: BlockInstance;
|
|
27
|
+
attrs: StandardProductBlockAttrs;
|
|
28
|
+
title: string | null;
|
|
29
|
+
}) => ReactNode | null;
|
|
30
|
+
};
|
|
31
|
+
export type StandardProductBlockRegistryOptions = {
|
|
32
|
+
useScopedKeys?: boolean;
|
|
33
|
+
includeReviewsTab?: boolean;
|
|
34
|
+
useShopReviewTabLabel?: boolean;
|
|
35
|
+
};
|
|
36
|
+
export type StandardProductBlockRegistryData = {
|
|
37
|
+
filteredDescription: string;
|
|
38
|
+
faqCount: number;
|
|
39
|
+
reviewCount: number;
|
|
40
|
+
reviewSource?: 'shop' | 'product';
|
|
41
|
+
relatedProductsCount: number;
|
|
42
|
+
};
|
|
43
|
+
export declare function createStandardProductBlockRegistry(input: {
|
|
44
|
+
slots: StandardProductBlockRegistrySlots;
|
|
45
|
+
data: StandardProductBlockRegistryData;
|
|
46
|
+
options?: StandardProductBlockRegistryOptions;
|
|
47
|
+
}): BuilderBlockRegistry<null>;
|
|
48
|
+
//# sourceMappingURL=standard-product-blocks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standard-product-blocks.d.ts","sourceRoot":"","sources":["../src/standard-product-blocks.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAKL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,OAAO,EACL,4BAA4B,EAC5B,oCAAoC,EACpC,sCAAsC,EACtC,8BAA8B,EAC9B,4BAA4B,EAC5B,2BAA2B,EAC3B,4BAA4B,EAC5B,mCAAmC,EACnC,6BAA6B,GAC9B,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACV,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,EACxB,2BAA2B,GAC5B,MAAM,4BAA4B,CAAC;AAEpC,KAAK,yBAAyB,GAAG,UAAU,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAElF,MAAM,MAAM,iCAAiC,GAAG;IAC9C,aAAa,EAAE,CAAC,KAAK,EAAE;QACrB,KAAK,EAAE,aAAa,CAAC;QACrB,KAAK,EAAE,yBAAyB,CAAC;KAClC,KAAK,SAAS,CAAC;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE;QACpB,KAAK,EAAE,aAAa,CAAC;QACrB,KAAK,EAAE,yBAAyB,CAAC;QACjC,MAAM,EAAE,oBAAoB,CAAC;KAC9B,KAAK,SAAS,CAAC;IAChB,aAAa,EAAE,CAAC,KAAK,EAAE;QACrB,KAAK,EAAE,aAAa,CAAC;QACrB,KAAK,EAAE,yBAAyB,CAAC;QACjC,MAAM,EAAE,qBAAqB,CAAC;QAC9B,IAAI,EAAE,sBAAsB,EAAE,CAAC;KAChC,KAAK,SAAS,CAAC;IAChB,qBAAqB,EAAE,CAAC,KAAK,EAAE;QAC7B,KAAK,EAAE,aAAa,CAAC;QACrB,KAAK,EAAE,yBAAyB,CAAC;QACjC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;KACtB,KAAK,SAAS,GAAG,IAAI,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,mCAAmC,GAAG;IAChD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG;IAC7C,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,oBAAoB,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,wBAAgB,kCAAkC,CAAC,KAAK,EAAE;IACxD,KAAK,EAAE,iCAAiC,CAAC;IACzC,IAAI,EAAE,gCAAgC,CAAC;IACvC,OAAO,CAAC,EAAE,mCAAmC,CAAC;CAC/C,GAAG,oBAAoB,CAAC,IAAI,CAAC,CA4C7B"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getProductPageBlockAttributes } from './product-page.js';
|
|
2
|
+
import { buildStandardProductInfoTabs, resolveStandardBuyBoxLabels, resolveStandardDetailsLabels, resolveStandardRelatedProductsTitle, } from './standard-product-page.js';
|
|
3
|
+
export { STANDARD_PRODUCT_BLOCK_TYPES, STANDARD_PRODUCT_PRIMARY_BLOCK_TYPES, STANDARD_PRODUCT_SECONDARY_BLOCK_TYPES, splitStandardProductPageBlocks, buildStandardProductInfoTabs, resolveStandardBuyBoxLabels, resolveStandardDetailsLabels, resolveStandardRelatedProductsTitle, resolveScopedBlockSettingText, } from './standard-product-page.js';
|
|
4
|
+
export function createStandardProductBlockRegistry(input) {
|
|
5
|
+
const { slots, data, options = {} } = input;
|
|
6
|
+
return {
|
|
7
|
+
gallery: ({ block }) => slots.renderGallery({
|
|
8
|
+
block,
|
|
9
|
+
attrs: getProductPageBlockAttributes(block),
|
|
10
|
+
}),
|
|
11
|
+
'buy-box': ({ block }) => slots.renderBuyBox({
|
|
12
|
+
block,
|
|
13
|
+
attrs: getProductPageBlockAttributes(block),
|
|
14
|
+
labels: resolveStandardBuyBoxLabels(block, options),
|
|
15
|
+
}),
|
|
16
|
+
details: ({ block }) => {
|
|
17
|
+
const labels = resolveStandardDetailsLabels(block, options);
|
|
18
|
+
const tabs = buildStandardProductInfoTabs({
|
|
19
|
+
labels,
|
|
20
|
+
filteredDescription: data.filteredDescription,
|
|
21
|
+
faqCount: data.faqCount,
|
|
22
|
+
reviewCount: data.reviewCount,
|
|
23
|
+
reviewSource: data.reviewSource,
|
|
24
|
+
includeReviewsTab: options.includeReviewsTab ?? true,
|
|
25
|
+
useShopReviewTabLabel: options.useShopReviewTabLabel ?? false,
|
|
26
|
+
});
|
|
27
|
+
return slots.renderDetails({
|
|
28
|
+
block,
|
|
29
|
+
attrs: getProductPageBlockAttributes(block),
|
|
30
|
+
labels,
|
|
31
|
+
tabs,
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
'related-products': ({ block }) => {
|
|
35
|
+
if (data.relatedProductsCount <= 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return slots.renderRelatedProducts({
|
|
39
|
+
block,
|
|
40
|
+
attrs: getProductPageBlockAttributes(block),
|
|
41
|
+
title: resolveStandardRelatedProductsTitle(block, options),
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { BlockInstance } from '@shoppex/builder-contracts';
|
|
2
|
+
export declare const STANDARD_PRODUCT_BLOCK_TYPES: readonly ["gallery", "buy-box", "details", "related-products"];
|
|
3
|
+
export type StandardProductBlockType = (typeof STANDARD_PRODUCT_BLOCK_TYPES)[number];
|
|
4
|
+
export declare const STANDARD_PRODUCT_PRIMARY_BLOCK_TYPES: readonly ["gallery", "buy-box"];
|
|
5
|
+
export declare const STANDARD_PRODUCT_SECONDARY_BLOCK_TYPES: readonly ["details", "related-products"];
|
|
6
|
+
export type StandardProductSettingScope = 'buyBox' | 'details' | 'relatedProducts';
|
|
7
|
+
export declare function splitStandardProductPageBlocks(blocks: BlockInstance[]): {
|
|
8
|
+
primary: {
|
|
9
|
+
id: string;
|
|
10
|
+
type: string;
|
|
11
|
+
visible: boolean;
|
|
12
|
+
settings: Record<string, unknown>;
|
|
13
|
+
variant?: string | undefined;
|
|
14
|
+
style_overrides?: Partial<Record<import("@shoppex/builder-contracts").StyleSlotId, import("@shoppex/builder-contracts").StyleSlotValue>> | undefined;
|
|
15
|
+
}[];
|
|
16
|
+
secondary: {
|
|
17
|
+
id: string;
|
|
18
|
+
type: string;
|
|
19
|
+
visible: boolean;
|
|
20
|
+
settings: Record<string, unknown>;
|
|
21
|
+
variant?: string | undefined;
|
|
22
|
+
style_overrides?: Partial<Record<import("@shoppex/builder-contracts").StyleSlotId, import("@shoppex/builder-contracts").StyleSlotValue>> | undefined;
|
|
23
|
+
}[];
|
|
24
|
+
};
|
|
25
|
+
export declare function resolveScopedBlockSettingText(block: Pick<BlockInstance, 'settings'>, scope: StandardProductSettingScope, key: string): string | null;
|
|
26
|
+
export type StandardBuyBoxLabels = {
|
|
27
|
+
variantLabel: string | null;
|
|
28
|
+
addonsLabel: string | null;
|
|
29
|
+
primaryActionLabel: string | null;
|
|
30
|
+
buyNowLabel: string | null;
|
|
31
|
+
quantityLabel: string | null;
|
|
32
|
+
reviewsLabel: string | null;
|
|
33
|
+
noReviewsLabel: string | null;
|
|
34
|
+
};
|
|
35
|
+
export declare function resolveStandardBuyBoxLabels(block: Pick<BlockInstance, 'settings'>, options?: {
|
|
36
|
+
useScopedKeys?: boolean;
|
|
37
|
+
}): StandardBuyBoxLabels;
|
|
38
|
+
export type StandardDetailsLabels = {
|
|
39
|
+
descriptionTabLabel: string | null;
|
|
40
|
+
reviewsTabLabel: string | null;
|
|
41
|
+
shopReviewsTabLabel: string | null;
|
|
42
|
+
faqTabLabel: string | null;
|
|
43
|
+
emptyDescriptionLabel: string | null;
|
|
44
|
+
emptyReviewsTitle: string | null;
|
|
45
|
+
emptyReviewsDescription: string | null;
|
|
46
|
+
emptyFaqLabel: string | null;
|
|
47
|
+
};
|
|
48
|
+
export declare function resolveStandardDetailsLabels(block: Pick<BlockInstance, 'settings'>, options?: {
|
|
49
|
+
useScopedKeys?: boolean;
|
|
50
|
+
}): StandardDetailsLabels;
|
|
51
|
+
export type StandardProductTabSpec = {
|
|
52
|
+
id: 'description' | 'reviews' | 'faq';
|
|
53
|
+
label: string;
|
|
54
|
+
content?: string;
|
|
55
|
+
badge?: string;
|
|
56
|
+
};
|
|
57
|
+
export declare function buildStandardProductInfoTabs(input: {
|
|
58
|
+
labels: StandardDetailsLabels;
|
|
59
|
+
filteredDescription: string;
|
|
60
|
+
faqCount: number;
|
|
61
|
+
reviewCount: number;
|
|
62
|
+
reviewSource?: 'shop' | 'product';
|
|
63
|
+
includeReviewsTab?: boolean;
|
|
64
|
+
useShopReviewTabLabel?: boolean;
|
|
65
|
+
}): StandardProductTabSpec[];
|
|
66
|
+
export declare function resolveStandardRelatedProductsTitle(block: Pick<BlockInstance, 'settings'>, options?: {
|
|
67
|
+
useScopedKeys?: boolean;
|
|
68
|
+
}): string | null;
|
|
69
|
+
//# sourceMappingURL=standard-product-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standard-product-page.d.ts","sourceRoot":"","sources":["../src/standard-product-page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGhE,eAAO,MAAM,4BAA4B,gEAK/B,CAAC;AAEX,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC;AAErF,eAAO,MAAM,oCAAoC,iCAAkC,CAAC;AACpF,eAAO,MAAM,sCAAsC,0CAA2C,CAAC;AAE/F,MAAM,MAAM,2BAA2B,GAAG,QAAQ,GAAG,SAAS,GAAG,iBAAiB,CAAC;AAQnF,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,aAAa,EAAE;;;;;;;;;;;;;;;;;EAKrE;AAED,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,EACtC,KAAK,EAAE,2BAA2B,EAClC,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,IAAI,CAIf;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,EACtC,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GACpC,oBAAoB,CAetB;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,CAAC;AAEF,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,EACtC,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GACpC,qBAAqB,CAgBvB;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,EAAE,EAAE,aAAa,GAAG,SAAS,GAAG,KAAK,CAAC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,MAAM,EAAE,qBAAqB,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,GAAG,sBAAsB,EAAE,CAyC3B;AAED,wBAAgB,mCAAmC,CACjD,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,EACtC,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GACpC,MAAM,GAAG,IAAI,CAKf"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getBuilderBlockSettingText } from './product-page.js';
|
|
2
|
+
export const STANDARD_PRODUCT_BLOCK_TYPES = [
|
|
3
|
+
'gallery',
|
|
4
|
+
'buy-box',
|
|
5
|
+
'details',
|
|
6
|
+
'related-products',
|
|
7
|
+
];
|
|
8
|
+
export const STANDARD_PRODUCT_PRIMARY_BLOCK_TYPES = ['gallery', 'buy-box'];
|
|
9
|
+
export const STANDARD_PRODUCT_SECONDARY_BLOCK_TYPES = ['details', 'related-products'];
|
|
10
|
+
const SCOPE_PREFIX = {
|
|
11
|
+
buyBox: 'product.buyBox',
|
|
12
|
+
details: 'product.details',
|
|
13
|
+
relatedProducts: 'product.relatedProducts',
|
|
14
|
+
};
|
|
15
|
+
export function splitStandardProductPageBlocks(blocks) {
|
|
16
|
+
return {
|
|
17
|
+
primary: blocks.filter((block) => block.type === 'gallery' || block.type === 'buy-box'),
|
|
18
|
+
secondary: blocks.filter((block) => block.type === 'details' || block.type === 'related-products'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function resolveScopedBlockSettingText(block, scope, key) {
|
|
22
|
+
const prefixed = getBuilderBlockSettingText(block, `${SCOPE_PREFIX[scope]}.${key}`);
|
|
23
|
+
if (prefixed)
|
|
24
|
+
return prefixed;
|
|
25
|
+
return getBuilderBlockSettingText(block, key);
|
|
26
|
+
}
|
|
27
|
+
export function resolveStandardBuyBoxLabels(block, options) {
|
|
28
|
+
const read = (key) => options?.useScopedKeys
|
|
29
|
+
? resolveScopedBlockSettingText(block, 'buyBox', key)
|
|
30
|
+
: getBuilderBlockSettingText(block, key);
|
|
31
|
+
return {
|
|
32
|
+
variantLabel: read('variantLabel'),
|
|
33
|
+
addonsLabel: read('addonsLabel'),
|
|
34
|
+
primaryActionLabel: read('primaryActionLabel'),
|
|
35
|
+
buyNowLabel: read('buyNowLabel'),
|
|
36
|
+
quantityLabel: read('quantityLabel'),
|
|
37
|
+
reviewsLabel: read('reviewsLabel'),
|
|
38
|
+
noReviewsLabel: read('noReviewsLabel'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function resolveStandardDetailsLabels(block, options) {
|
|
42
|
+
const read = (key) => options?.useScopedKeys
|
|
43
|
+
? resolveScopedBlockSettingText(block, 'details', key)
|
|
44
|
+
: getBuilderBlockSettingText(block, key);
|
|
45
|
+
return {
|
|
46
|
+
descriptionTabLabel: read('descriptionTabLabel'),
|
|
47
|
+
reviewsTabLabel: read('reviewsTabLabel'),
|
|
48
|
+
shopReviewsTabLabel: read('shopReviewsTabLabel'),
|
|
49
|
+
faqTabLabel: read('faqTabLabel'),
|
|
50
|
+
emptyDescriptionLabel: read('emptyDescriptionLabel'),
|
|
51
|
+
emptyReviewsTitle: read('emptyReviewsTitle'),
|
|
52
|
+
emptyReviewsDescription: read('emptyReviewsDescription'),
|
|
53
|
+
emptyFaqLabel: read('emptyFaqLabel'),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function buildStandardProductInfoTabs(input) {
|
|
57
|
+
const { labels, filteredDescription, faqCount, reviewCount, reviewSource = 'product', includeReviewsTab = true, useShopReviewTabLabel = false, } = input;
|
|
58
|
+
const tabs = [
|
|
59
|
+
{
|
|
60
|
+
id: 'description',
|
|
61
|
+
label: labels.descriptionTabLabel ?? 'Description',
|
|
62
|
+
content: filteredDescription,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
if (includeReviewsTab) {
|
|
66
|
+
const reviewsLabel = useShopReviewTabLabel && reviewSource === 'shop'
|
|
67
|
+
? (labels.shopReviewsTabLabel ?? labels.reviewsTabLabel ?? 'Shop Reviews')
|
|
68
|
+
: (labels.reviewsTabLabel ?? 'Reviews');
|
|
69
|
+
tabs.push({
|
|
70
|
+
id: 'reviews',
|
|
71
|
+
label: reviewsLabel,
|
|
72
|
+
badge: String(reviewCount),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (faqCount > 0) {
|
|
76
|
+
tabs.push({
|
|
77
|
+
id: 'faq',
|
|
78
|
+
label: labels.faqTabLabel ?? 'FAQ',
|
|
79
|
+
badge: String(faqCount),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return tabs;
|
|
83
|
+
}
|
|
84
|
+
export function resolveStandardRelatedProductsTitle(block, options) {
|
|
85
|
+
if (options?.useScopedKeys) {
|
|
86
|
+
return resolveScopedBlockSettingText(block, 'relatedProducts', 'title');
|
|
87
|
+
}
|
|
88
|
+
return getBuilderBlockSettingText(block, 'title');
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storefront-google-fonts.d.ts","sourceRoot":"","sources":["../src/storefront-google-fonts.ts"],"names":[],"mappings":"AAEA,wBAAgB,mCAAmC,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,IAAI,CAwC/E"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const STOREFRONT_GOOGLE_FONT_MARKER = 'data-shoppex-storefront-google-font';
|
|
2
|
+
export function syncStorefrontGoogleFontStylesheets(hrefs) {
|
|
3
|
+
if (typeof document === 'undefined') {
|
|
4
|
+
return () => { };
|
|
5
|
+
}
|
|
6
|
+
const nextHrefs = [...new Set(hrefs.filter((href) => href.trim().length > 0))];
|
|
7
|
+
const existing = Array.from(document.head.querySelectorAll(`link[rel="stylesheet"][${STOREFRONT_GOOGLE_FONT_MARKER}]`));
|
|
8
|
+
for (const link of existing) {
|
|
9
|
+
if (!nextHrefs.includes(link.href)) {
|
|
10
|
+
link.remove();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
for (const href of nextHrefs) {
|
|
14
|
+
const alreadyPresent = Array.from(document.head.querySelectorAll('link[rel="stylesheet"]')).some((link) => link.href === href);
|
|
15
|
+
if (alreadyPresent)
|
|
16
|
+
continue;
|
|
17
|
+
const link = document.createElement('link');
|
|
18
|
+
link.rel = 'stylesheet';
|
|
19
|
+
link.href = href;
|
|
20
|
+
link.setAttribute(STOREFRONT_GOOGLE_FONT_MARKER, 'true');
|
|
21
|
+
document.head.appendChild(link);
|
|
22
|
+
}
|
|
23
|
+
return () => {
|
|
24
|
+
for (const link of Array.from(document.head.querySelectorAll(`link[rel="stylesheet"][${STOREFRONT_GOOGLE_FONT_MARKER}]`))) {
|
|
25
|
+
link.remove();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shoppexio/builder-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Theme-side Builder v2 runtime helpers for Shoppex storefront themes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -75,11 +75,11 @@
|
|
|
75
75
|
"author": "Shoppex",
|
|
76
76
|
"license": "MIT",
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@shoppexio/builder-contracts": "0.1.
|
|
78
|
+
"@shoppexio/builder-contracts": "0.1.1",
|
|
79
79
|
"react": "^18.0.0 || ^19.0.0"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@shoppex/builder-contracts": "npm:@shoppexio/builder-contracts@0.1.
|
|
82
|
+
"@shoppex/builder-contracts": "npm:@shoppexio/builder-contracts@0.1.1"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"jsdom": "^28.1.0",
|
|
@@ -118,6 +118,39 @@ describe('@shoppex/builder-runtime', () => {
|
|
|
118
118
|
}, 'product')).toEqual(['gallery', 'buy-box']);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
test('reads legacy builder.pages manifests used by custom imported themes', () => {
|
|
122
|
+
expect(getThemePageBlockOrderFromManifest({
|
|
123
|
+
id: 'cheatshub',
|
|
124
|
+
name: 'CheatsHub Theme',
|
|
125
|
+
version: '0.1.0',
|
|
126
|
+
builder: {
|
|
127
|
+
pages: [
|
|
128
|
+
{
|
|
129
|
+
id: 'home',
|
|
130
|
+
label: 'Home',
|
|
131
|
+
blocks: ['marquee', 'hero', 'products'],
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
blocks: {
|
|
135
|
+
marquee: { label: 'Marquee', settings: {}, variants: [], exposedStyleSlots: [], presets: [] },
|
|
136
|
+
hero: { label: 'Hero', settings: {}, variants: [], exposedStyleSlots: [], presets: [] },
|
|
137
|
+
products: { label: 'Products', settings: {}, variants: [], exposedStyleSlots: [], presets: [] },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
}, 'home')).toEqual(['marquee', 'hero', 'products']);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('ignores malformed defaultBlocks in raw page manifests', () => {
|
|
144
|
+
expect(getThemePageBlockOrderFromManifest({
|
|
145
|
+
pages: {
|
|
146
|
+
home: {
|
|
147
|
+
allowedBlocks: ['hero', 'faq'],
|
|
148
|
+
defaultBlocks: { type: 'hero' },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}, 'home')).toEqual(['hero', 'faq']);
|
|
152
|
+
});
|
|
153
|
+
|
|
121
154
|
test('resolves style slots with breakpoint fallback and block override', () => {
|
|
122
155
|
const settings = createSettings();
|
|
123
156
|
const block = settings.theme.layout.home.blocks[0];
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
export * from './preview-fixtures.js';
|
|
2
|
+
export * from './product-page.js';
|
|
3
|
+
export * from './standard-product-page.js';
|
|
1
4
|
export * from './attributes.js';
|
|
2
5
|
export * from './content.js';
|
|
3
6
|
export * from './css-vars.js';
|
|
4
7
|
export * from './layout.js';
|
|
5
8
|
export * from './react.js';
|
|
9
|
+
export * from './storefront-google-fonts.js';
|
|
6
10
|
export * from './style-slots.js';
|
package/src/layout.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
getThemePageBlockOrderFromManifest as resolveThemePageBlockOrder,
|
|
3
|
+
type BlockInstance,
|
|
4
|
+
type BuilderSettings,
|
|
5
|
+
type PageLayout,
|
|
6
|
+
type ThemeManifest,
|
|
7
|
+
type ThemePageBlockOrderPage,
|
|
8
|
+
} from '@shoppex/builder-contracts';
|
|
2
9
|
|
|
3
10
|
export type ThemePageBlockOrderManifest = {
|
|
4
|
-
pages?: Record<string,
|
|
5
|
-
allowedBlocks?: string[];
|
|
6
|
-
defaultBlocks?: Array<{ type?: string }>;
|
|
7
|
-
}>;
|
|
11
|
+
pages?: Record<string, ThemePageBlockOrderPage>;
|
|
8
12
|
};
|
|
9
13
|
|
|
14
|
+
export { resolveThemePageBlockOrder as getThemePageBlockOrderFromManifest };
|
|
15
|
+
|
|
10
16
|
export function getPageLayout(settings: BuilderSettings, pageId: string): PageLayout {
|
|
11
17
|
return settings.theme.layout[pageId] ?? { blocks: [] };
|
|
12
18
|
}
|
|
@@ -27,22 +33,6 @@ export function getAllowedBlockTypes(manifest: ThemeManifest, pageId: string): s
|
|
|
27
33
|
return manifest.pages[pageId]?.allowedBlocks ?? [];
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
export function getThemePageBlockOrderFromManifest(
|
|
31
|
-
manifest: ThemePageBlockOrderManifest,
|
|
32
|
-
pageId: string,
|
|
33
|
-
): string[] {
|
|
34
|
-
const page = manifest.pages?.[pageId];
|
|
35
|
-
if (!page) {
|
|
36
|
-
return [];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const defaultBlockTypes = (page.defaultBlocks ?? [])
|
|
40
|
-
.map((block) => block.type)
|
|
41
|
-
.filter((blockType): blockType is string => typeof blockType === 'string' && blockType.length > 0);
|
|
42
|
-
|
|
43
|
-
return defaultBlockTypes.length > 0 ? defaultBlockTypes : page.allowedBlocks ?? [];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
36
|
export function canAddBlock(settings: BuilderSettings, manifest: ThemeManifest, pageId: string, blockType: string): boolean {
|
|
47
37
|
const page = manifest.pages[pageId];
|
|
48
38
|
const blockDefinition = manifest.blocks[blockType];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type BuilderPreviewReview = {
|
|
2
|
+
id: string;
|
|
3
|
+
author: string | null;
|
|
4
|
+
comment: string | null;
|
|
5
|
+
rating: number | null;
|
|
6
|
+
created_at: string;
|
|
7
|
+
is_automated?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type BuilderPreviewFaqItem = {
|
|
11
|
+
question: string;
|
|
12
|
+
answer: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const BUILDER_PREVIEW_REVIEWS: BuilderPreviewReview[] = [
|
|
16
|
+
{
|
|
17
|
+
id: 'preview-review-1',
|
|
18
|
+
author: 'Alex M.',
|
|
19
|
+
comment: 'Instant delivery and clear instructions. Would buy again.',
|
|
20
|
+
rating: 5,
|
|
21
|
+
created_at: '2026-04-12T10:00:00.000Z',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'preview-review-2',
|
|
25
|
+
author: 'Jamie R.',
|
|
26
|
+
comment: 'Support answered quickly when I had a setup question.',
|
|
27
|
+
rating: 5,
|
|
28
|
+
created_at: '2026-04-03T14:30:00.000Z',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'preview-review-3',
|
|
32
|
+
author: 'Taylor S.',
|
|
33
|
+
comment: 'Smooth checkout experience and exactly what was advertised.',
|
|
34
|
+
rating: 4,
|
|
35
|
+
created_at: '2026-03-22T09:15:00.000Z',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export function getBuilderPreviewReviewFixtures<T = BuilderPreviewReview>(): T[] {
|
|
40
|
+
return BUILDER_PREVIEW_REVIEWS as T[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const BUILDER_PREVIEW_FAQ_ITEMS: BuilderPreviewFaqItem[] = [
|
|
44
|
+
{
|
|
45
|
+
question: 'How fast is delivery?',
|
|
46
|
+
answer: 'Most digital products are delivered instantly after payment confirmation.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
question: 'Which payment methods do you accept?',
|
|
50
|
+
answer: 'Available payment methods depend on your shop configuration and region.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
question: 'How do I get support?',
|
|
54
|
+
answer: 'Use the contact page or your customer portal for order-related help.',
|
|
55
|
+
},
|
|
56
|
+
];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { createBlockInstance } from '@shoppex/builder-contracts';
|
|
3
|
+
import {
|
|
4
|
+
getBuilderBlockSettingText,
|
|
5
|
+
getLayoutPageBlockAttributes,
|
|
6
|
+
getProductPageBlockAttributes,
|
|
7
|
+
} from './product-page.js';
|
|
8
|
+
|
|
9
|
+
describe('product-page helpers', () => {
|
|
10
|
+
test('getBuilderBlockSettingText trims non-empty strings', () => {
|
|
11
|
+
const block = createBlockInstance({
|
|
12
|
+
type: 'buy-box',
|
|
13
|
+
settings: {
|
|
14
|
+
variantLabel: ' License Type ',
|
|
15
|
+
addonsLabel: ' ',
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(getBuilderBlockSettingText(block, 'variantLabel')).toBe('License Type');
|
|
20
|
+
expect(getBuilderBlockSettingText(block, 'addonsLabel')).toBeNull();
|
|
21
|
+
expect(getBuilderBlockSettingText(block, 'missing')).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('getLayoutPageBlockAttributes includes page id and builder block attrs', () => {
|
|
25
|
+
const block = createBlockInstance({ id: 'gallery-1', type: 'gallery' });
|
|
26
|
+
expect(getProductPageBlockAttributes(block)).toEqual({
|
|
27
|
+
'data-page-id': 'product',
|
|
28
|
+
'data-builder-block': 'gallery-1',
|
|
29
|
+
'data-builder-block-type': 'gallery',
|
|
30
|
+
});
|
|
31
|
+
expect(getLayoutPageBlockAttributes('reviews-page', block)).toEqual({
|
|
32
|
+
'data-page-id': 'reviews-page',
|
|
33
|
+
'data-builder-block': 'gallery-1',
|
|
34
|
+
'data-builder-block-type': 'gallery',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BlockInstance } from '@shoppex/builder-contracts';
|
|
2
|
+
import { builderBlock } from './attributes.js';
|
|
3
|
+
|
|
4
|
+
export function getBuilderBlockSettingText(
|
|
5
|
+
block: Pick<BlockInstance, 'settings'>,
|
|
6
|
+
key: string,
|
|
7
|
+
): string | null {
|
|
8
|
+
const value = block.settings[key];
|
|
9
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getLayoutPageBlockAttributes(
|
|
13
|
+
pageId: string,
|
|
14
|
+
block: Pick<BlockInstance, 'id' | 'type'>,
|
|
15
|
+
) {
|
|
16
|
+
return {
|
|
17
|
+
'data-page-id': pageId,
|
|
18
|
+
...builderBlock(block.id, block.type),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getProductPageBlockAttributes(
|
|
23
|
+
block: Pick<BlockInstance, 'id' | 'type'>,
|
|
24
|
+
) {
|
|
25
|
+
return getLayoutPageBlockAttributes('product', block);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @deprecated Use getBuilderBlockSettingText */
|
|
29
|
+
export const getProductBlockText = getBuilderBlockSettingText;
|
|
30
|
+
|
|
31
|
+
/** @deprecated Use getProductPageBlockAttributes */
|
|
32
|
+
export const getBuilderProductBlockAttributes = getProductPageBlockAttributes;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
useBuilderContent,
|
|
13
13
|
useBuilderContentRecord,
|
|
14
14
|
useThemePageBlocks,
|
|
15
|
+
useThemePageBlockAttributes,
|
|
15
16
|
} from './react.js';
|
|
16
17
|
|
|
17
18
|
function createSettings(revision: number, title: string): BuilderSettings {
|
|
@@ -113,6 +114,18 @@ function PageBlocksProbe({ pageId = 'home', defaultOrder = ['hero', 'products']
|
|
|
113
114
|
);
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
function PageBlockAttributesProbe({
|
|
118
|
+
pageId = 'contact-page',
|
|
119
|
+
defaultOrder = ['page-contact-page'],
|
|
120
|
+
}: {
|
|
121
|
+
pageId?: string;
|
|
122
|
+
defaultOrder?: string[];
|
|
123
|
+
}) {
|
|
124
|
+
const attrs = useThemePageBlockAttributes(pageId, defaultOrder);
|
|
125
|
+
|
|
126
|
+
return <section data-testid="page-block-attrs" {...attrs} />;
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
function HeroBlock({ block }: { block: BlockInstance }) {
|
|
117
130
|
const title = useBuilderContent('hero.title', '');
|
|
118
131
|
|
|
@@ -468,6 +481,35 @@ describe('BuilderRuntimePreviewProvider', () => {
|
|
|
468
481
|
expect(dom.window.document.querySelector('[data-testid="page-blocks"]')?.textContent).toBe('hero-1:hero');
|
|
469
482
|
});
|
|
470
483
|
|
|
484
|
+
test('useThemePageBlockAttributes exposes page id and first block attrs', async () => {
|
|
485
|
+
await act(async () => {
|
|
486
|
+
root.render(
|
|
487
|
+
<BuilderRuntimePreviewProvider
|
|
488
|
+
initialSettings={{
|
|
489
|
+
...createEmptyBuilderSettings(1),
|
|
490
|
+
theme: {
|
|
491
|
+
...createEmptyBuilderSettings(1).theme,
|
|
492
|
+
layout: {
|
|
493
|
+
'contact-page': {
|
|
494
|
+
blocks: [
|
|
495
|
+
{ id: 'contact-1', type: 'page-contact-page', visible: true, settings: {} },
|
|
496
|
+
],
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
}}
|
|
501
|
+
>
|
|
502
|
+
<PageBlockAttributesProbe />
|
|
503
|
+
</BuilderRuntimePreviewProvider>,
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const section = dom.window.document.querySelector('[data-testid="page-block-attrs"]');
|
|
508
|
+
expect(section?.getAttribute('data-page-id')).toBe('contact-page');
|
|
509
|
+
expect(section?.getAttribute('data-builder-block')).toBe('contact-1');
|
|
510
|
+
expect(section?.getAttribute('data-builder-block-type')).toBe('page-contact-page');
|
|
511
|
+
});
|
|
512
|
+
|
|
471
513
|
test('shows missing registry blocks inside trusted builder preview', async () => {
|
|
472
514
|
await act(async () => {
|
|
473
515
|
root.render(
|