@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.
Files changed (44) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -0
  4. package/dist/layout.d.ts +3 -8
  5. package/dist/layout.d.ts.map +1 -1
  6. package/dist/layout.js +2 -10
  7. package/dist/preview-fixtures.d.ts +16 -0
  8. package/dist/preview-fixtures.d.ts.map +1 -0
  9. package/dist/preview-fixtures.js +40 -0
  10. package/dist/product-page.d.ts +13 -0
  11. package/dist/product-page.d.ts.map +1 -0
  12. package/dist/product-page.js +18 -0
  13. package/dist/react.d.ts +31 -2
  14. package/dist/react.d.ts.map +1 -1
  15. package/dist/react.js +122 -42
  16. package/dist/search-bar-settings.d.ts +33 -0
  17. package/dist/search-bar-settings.d.ts.map +1 -0
  18. package/dist/search-bar-settings.js +99 -0
  19. package/dist/standard-product-blocks.d.ts +48 -0
  20. package/dist/standard-product-blocks.d.ts.map +1 -0
  21. package/dist/standard-product-blocks.js +45 -0
  22. package/dist/standard-product-page.d.ts +69 -0
  23. package/dist/standard-product-page.d.ts.map +1 -0
  24. package/dist/standard-product-page.js +89 -0
  25. package/dist/storefront-google-fonts.d.ts +2 -0
  26. package/dist/storefront-google-fonts.d.ts.map +1 -0
  27. package/dist/storefront-google-fonts.js +28 -0
  28. package/package.json +3 -3
  29. package/src/builder-runtime.test.ts +33 -0
  30. package/src/index.ts +4 -0
  31. package/src/layout.ts +11 -21
  32. package/src/preview-fixtures.ts +56 -0
  33. package/src/product-page.test.ts +37 -0
  34. package/src/product-page.ts +32 -0
  35. package/src/react-runtime.test.tsx +42 -0
  36. package/src/react.tsx +214 -45
  37. package/src/search-bar-settings.test.ts +72 -0
  38. package/src/search-bar-settings.ts +176 -0
  39. package/src/standard-product-blocks.test.tsx +93 -0
  40. package/src/standard-product-blocks.tsx +121 -0
  41. package/src/standard-product-page.test.ts +171 -0
  42. package/src/standard-product-page.ts +169 -0
  43. package/src/storefront-google-fonts.test.ts +31 -0
  44. 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,2 @@
1
+ export declare function syncStorefrontGoogleFontStylesheets(hrefs: string[]): () => void;
2
+ //# sourceMappingURL=storefront-google-fonts.d.ts.map
@@ -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.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.0",
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.0"
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 type { BlockInstance, BuilderSettings, PageLayout, ThemeManifest } from '@shoppex/builder-contracts';
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(