@shoppexio/builder-runtime 0.1.0 → 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 (53) hide show
  1. package/dist/css-vars.d.ts.map +1 -1
  2. package/dist/css-vars.js +24 -6
  3. package/dist/index.d.ts +4 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +4 -0
  6. package/dist/layout.d.ts +5 -1
  7. package/dist/layout.d.ts.map +1 -1
  8. package/dist/layout.js +2 -0
  9. package/dist/preview-fixtures.d.ts +16 -0
  10. package/dist/preview-fixtures.d.ts.map +1 -0
  11. package/dist/preview-fixtures.js +40 -0
  12. package/dist/product-page.d.ts +13 -0
  13. package/dist/product-page.d.ts.map +1 -0
  14. package/dist/product-page.js +18 -0
  15. package/dist/react.d.ts +36 -224
  16. package/dist/react.d.ts.map +1 -1
  17. package/dist/react.js +606 -47
  18. package/dist/search-bar-settings.d.ts +33 -0
  19. package/dist/search-bar-settings.d.ts.map +1 -0
  20. package/dist/search-bar-settings.js +99 -0
  21. package/dist/standard-product-blocks.d.ts +48 -0
  22. package/dist/standard-product-blocks.d.ts.map +1 -0
  23. package/dist/standard-product-blocks.js +45 -0
  24. package/dist/standard-product-page.d.ts +69 -0
  25. package/dist/standard-product-page.d.ts.map +1 -0
  26. package/dist/standard-product-page.js +89 -0
  27. package/dist/storefront-google-fonts.d.ts +2 -0
  28. package/dist/storefront-google-fonts.d.ts.map +1 -0
  29. package/dist/storefront-google-fonts.js +28 -0
  30. package/package.json +3 -3
  31. package/src/builder-runtime.test.ts +57 -0
  32. package/src/css-vars.ts +29 -8
  33. package/src/index.ts +4 -0
  34. package/src/layout.ts +14 -1
  35. package/src/preview-fixtures.ts +56 -0
  36. package/src/product-page.test.ts +37 -0
  37. package/src/product-page.ts +32 -0
  38. package/src/react-runtime.test.tsx +215 -3
  39. package/src/react.tsx +769 -45
  40. package/src/search-bar-settings.test.ts +72 -0
  41. package/src/search-bar-settings.ts +176 -0
  42. package/src/standard-product-blocks.test.tsx +93 -0
  43. package/src/standard-product-blocks.tsx +121 -0
  44. package/src/standard-product-page.test.ts +171 -0
  45. package/src/standard-product-page.ts +169 -0
  46. package/src/storefront-google-fonts.test.ts +31 -0
  47. package/src/storefront-google-fonts.ts +43 -0
  48. package/dist/builder-runtime.test.d.ts +0 -2
  49. package/dist/builder-runtime.test.d.ts.map +0 -1
  50. package/dist/builder-runtime.test.js +0 -115
  51. package/dist/react-runtime.test.d.ts +0 -2
  52. package/dist/react-runtime.test.d.ts.map +0 -1
  53. package/dist/react-runtime.test.js +0 -292
@@ -0,0 +1,33 @@
1
+ import type { BuilderSettings } from '@shoppex/builder-contracts';
2
+ export declare const SEARCH_BAR_MAX_WIDTH: Record<string, string>;
3
+ export type ResolvedSearchBarSettings = {
4
+ placeholder: string;
5
+ backgroundColor?: string;
6
+ borderColor?: string;
7
+ borderRadiusPx?: number;
8
+ maxWidth: string;
9
+ showShortcut: boolean;
10
+ showInHero: boolean;
11
+ };
12
+ export declare function getNavigationHeaderSettings(settings: BuilderSettings): Record<string, unknown>;
13
+ export declare function readSetting(sources: Array<Record<string, unknown> | undefined>, path: string): unknown;
14
+ export declare function isValidHexColor(value: unknown): value is string;
15
+ export declare function normalizeHexColor(value: string): string;
16
+ export declare function resolveSearchColor(value: unknown): string | undefined;
17
+ export declare function resolveSearchBorderRadius(value: unknown): number | undefined;
18
+ export declare function resolveSearchMaxWidth(value: unknown, fallback?: string): string;
19
+ export declare function resolveSearchBoolean(value: unknown, fallback: boolean): boolean;
20
+ export declare function resolveSearchPlaceholder(value: unknown, fallback: string): string;
21
+ export declare function resolveSearchBarSettings(input: {
22
+ variant: 'hero' | 'navigation';
23
+ heroValues: Record<string, unknown>;
24
+ headerSettings: Record<string, unknown>;
25
+ }): ResolvedSearchBarSettings;
26
+ export declare function buildSearchShellStyle(settings: Pick<ResolvedSearchBarSettings, 'backgroundColor' | 'borderColor' | 'borderRadiusPx'>): {
27
+ backgroundColor?: string;
28
+ borderColor?: string;
29
+ borderWidth?: string;
30
+ borderStyle?: 'solid';
31
+ borderRadius?: string;
32
+ } | undefined;
33
+ //# sourceMappingURL=search-bar-settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-bar-settings.d.ts","sourceRoot":"","sources":["../src/search-bar-settings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKvD,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,eAAe,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKzB;AAED,wBAAgB,WAAW,CACzB,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,EACnD,IAAI,EAAE,MAAM,GACX,OAAO,CAUT;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAG/D;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAGrE;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAG5E;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAO,GAAG,MAAM,CAK7E;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAG/E;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GACf,MAAM,CAER;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,OAAO,EAAE,MAAM,GAAG,YAAY,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC,GAAG,yBAAyB,CAwD5B;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,IAAI,CACZ,yBAAyB,EACzB,iBAAiB,GAAG,aAAa,GAAG,gBAAgB,CACrD,GACA;IACD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,SAAS,CAoBZ"}
@@ -0,0 +1,99 @@
1
+ export const SEARCH_BAR_MAX_WIDTH = {
2
+ sm: '20rem',
3
+ md: '28rem',
4
+ lg: '36rem',
5
+ full: '100%',
6
+ };
7
+ export function getNavigationHeaderSettings(settings) {
8
+ const block = settings.theme.layout.navigation?.blocks.find((candidate) => candidate.type === 'header');
9
+ return block?.settings ?? {};
10
+ }
11
+ export function readSetting(sources, path) {
12
+ for (const source of sources) {
13
+ if (!source)
14
+ continue;
15
+ if (!Object.prototype.hasOwnProperty.call(source, path))
16
+ continue;
17
+ const value = source[path];
18
+ if (value !== undefined && value !== null && value !== '') {
19
+ return value;
20
+ }
21
+ }
22
+ return undefined;
23
+ }
24
+ export function isValidHexColor(value) {
25
+ if (typeof value !== 'string' || !value.trim())
26
+ return false;
27
+ return /^#?[a-f\d]{3}(?:[a-f\d]{3})?(?:[a-f\d]{2})?$/i.test(value.trim());
28
+ }
29
+ export function normalizeHexColor(value) {
30
+ const trimmed = value.trim();
31
+ return trimmed.startsWith('#') ? trimmed : `#${trimmed}`;
32
+ }
33
+ export function resolveSearchColor(value) {
34
+ if (!isValidHexColor(value))
35
+ return undefined;
36
+ return normalizeHexColor(value);
37
+ }
38
+ export function resolveSearchBorderRadius(value) {
39
+ if (typeof value !== 'number' || !Number.isFinite(value))
40
+ return undefined;
41
+ return Math.max(0, Math.round(value));
42
+ }
43
+ export function resolveSearchMaxWidth(value, fallback = 'md') {
44
+ if (typeof value === 'string' && value in SEARCH_BAR_MAX_WIDTH) {
45
+ return SEARCH_BAR_MAX_WIDTH[value];
46
+ }
47
+ return SEARCH_BAR_MAX_WIDTH[fallback] ?? SEARCH_BAR_MAX_WIDTH.md;
48
+ }
49
+ export function resolveSearchBoolean(value, fallback) {
50
+ if (typeof value === 'boolean')
51
+ return value;
52
+ return fallback;
53
+ }
54
+ export function resolveSearchPlaceholder(value, fallback) {
55
+ return typeof value === 'string' && value.trim() ? value : fallback;
56
+ }
57
+ export function resolveSearchBarSettings(input) {
58
+ const navSources = [input.headerSettings];
59
+ const heroSources = [input.heroValues, input.headerSettings];
60
+ if (input.variant === 'hero') {
61
+ return {
62
+ placeholder: resolveSearchPlaceholder(readSetting(heroSources, 'hero.search.placeholder') ??
63
+ readSetting(navSources, 'navigation.search.placeholder'), 'Search products…'),
64
+ backgroundColor: resolveSearchColor(readSetting(heroSources, 'hero.search.background') ??
65
+ readSetting(navSources, 'navigation.search.background')),
66
+ borderColor: resolveSearchColor(readSetting(heroSources, 'hero.search.borderColor') ??
67
+ readSetting(navSources, 'navigation.search.borderColor')),
68
+ borderRadiusPx: resolveSearchBorderRadius(readSetting(heroSources, 'hero.search.borderRadius') ??
69
+ readSetting(navSources, 'navigation.search.borderRadius')),
70
+ maxWidth: resolveSearchMaxWidth(readSetting(heroSources, 'hero.search.maxWidth'), 'md'),
71
+ showShortcut: resolveSearchBoolean(readSetting(heroSources, 'hero.search.showShortcut'), true),
72
+ showInHero: resolveSearchBoolean(readSetting(heroSources, 'hero.search.show'), true),
73
+ };
74
+ }
75
+ return {
76
+ placeholder: resolveSearchPlaceholder(readSetting(navSources, 'navigation.search.placeholder'), 'Search products…'),
77
+ backgroundColor: resolveSearchColor(readSetting(navSources, 'navigation.search.background')),
78
+ borderColor: resolveSearchColor(readSetting(navSources, 'navigation.search.borderColor')),
79
+ borderRadiusPx: resolveSearchBorderRadius(readSetting(navSources, 'navigation.search.borderRadius')),
80
+ maxWidth: SEARCH_BAR_MAX_WIDTH.md,
81
+ showShortcut: true,
82
+ showInHero: true,
83
+ };
84
+ }
85
+ export function buildSearchShellStyle(settings) {
86
+ const style = {};
87
+ if (settings.backgroundColor) {
88
+ style.backgroundColor = settings.backgroundColor;
89
+ }
90
+ if (settings.borderColor) {
91
+ style.borderColor = settings.borderColor;
92
+ style.borderWidth = '1px';
93
+ style.borderStyle = 'solid';
94
+ }
95
+ if (settings.borderRadiusPx !== undefined) {
96
+ style.borderRadius = `${settings.borderRadiusPx}px`;
97
+ }
98
+ return Object.keys(style).length > 0 ? style : undefined;
99
+ }
@@ -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.0",
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",
@@ -9,6 +9,7 @@ import {
9
9
  getBuilderContentList,
10
10
  getBuilderContentString,
11
11
  getPageBlocks,
12
+ getThemePageBlockOrderFromManifest,
12
13
  resolveBlockSettings,
13
14
  resolveStyleSlotValue,
14
15
  } from './index.js';
@@ -40,6 +41,7 @@ function createSettings(): BuilderSettings {
40
41
  style_slots: {
41
42
  'button.radius': { base: 8, md: 12 },
42
43
  'color.primary': '#ff5500',
44
+ 'theme.hero.overlay.opacity': 0.72,
43
45
  },
44
46
  pages: [],
45
47
  terms: {},
@@ -95,6 +97,60 @@ describe('@shoppex/builder-runtime', () => {
95
97
  expect(getPageBlocks(settings, 'missing')).toEqual([]);
96
98
  });
97
99
 
100
+ test('reads canonical default page block order from a manifest', () => {
101
+ expect(getThemePageBlockOrderFromManifest({
102
+ pages: {
103
+ home: {
104
+ allowedBlocks: ['hero', 'faq'],
105
+ defaultBlocks: [{ type: 'hero' }],
106
+ },
107
+ product: {
108
+ allowedBlocks: ['gallery', 'buy-box'],
109
+ },
110
+ },
111
+ }, 'home')).toEqual(['hero']);
112
+ expect(getThemePageBlockOrderFromManifest({
113
+ pages: {
114
+ product: {
115
+ allowedBlocks: ['gallery', 'buy-box'],
116
+ },
117
+ },
118
+ }, 'product')).toEqual(['gallery', 'buy-box']);
119
+ });
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
+
98
154
  test('resolves style slots with breakpoint fallback and block override', () => {
99
155
  const settings = createSettings();
100
156
  const block = settings.theme.layout.home.blocks[0];
@@ -108,6 +164,7 @@ describe('@shoppex/builder-runtime', () => {
108
164
 
109
165
  expect(css).toContain('--builder-button-radius: 8px;');
110
166
  expect(css).toContain('--builder-color-primary: #ff5500;');
167
+ expect(css).toContain('--builder-theme-hero-overlay-opacity: 0.72;');
111
168
  expect(css).toContain('@media (min-width: 768px)');
112
169
  expect(css).toContain('--builder-button-radius: 12px;');
113
170
  });
package/src/css-vars.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BuilderSettings, Breakpoint, StyleSlotId, StyleSlots } from '@shoppex/builder-contracts';
1
+ import type { BuilderSettings, Breakpoint, CoreStyleSlotId, StyleSlotId, StyleSlots } from '@shoppex/builder-contracts';
2
2
  import { CORE_STYLE_SLOT_IDS } from '@shoppex/builder-contracts';
3
3
  import { isResponsiveRecord } from './style-slots.js';
4
4
 
@@ -9,7 +9,7 @@ const BREAKPOINT_MEDIA: Record<Exclude<Breakpoint, 'base'>, string> = {
9
9
  xl: '(min-width: 1280px)',
10
10
  };
11
11
 
12
- const STYLE_SLOT_CSS_VARIABLES: Record<StyleSlotId, { name: string; unit?: string }> = {
12
+ const STYLE_SLOT_CSS_VARIABLES: Record<CoreStyleSlotId, { name: string; unit?: string }> = {
13
13
  'button.radius': { name: '--builder-button-radius', unit: 'px' },
14
14
  'button.background': { name: '--builder-button-background' },
15
15
  'button.foreground': { name: '--builder-button-foreground' },
@@ -37,19 +37,19 @@ const STYLE_SLOT_CSS_VARIABLES: Record<StyleSlotId, { name: string; unit?: strin
37
37
  };
38
38
 
39
39
  export function getStyleSlotCssVariable(slotId: StyleSlotId): string {
40
- return STYLE_SLOT_CSS_VARIABLES[slotId].name;
40
+ return getStyleSlotCssVariableConfig(slotId).name;
41
41
  }
42
42
 
43
43
  export function createStyleSlotCssVariables(slots: StyleSlots): Record<string, string> {
44
44
  const variables: Record<string, string> = {};
45
45
 
46
- for (const slotId of CORE_STYLE_SLOT_IDS) {
46
+ for (const slotId of getRenderableStyleSlotIds(slots)) {
47
47
  const value = slots[slotId];
48
48
  if (value === undefined || isResponsiveRecord(value)) {
49
49
  continue;
50
50
  }
51
51
 
52
- variables[STYLE_SLOT_CSS_VARIABLES[slotId].name] = formatStyleSlotValue(slotId, value);
52
+ variables[getStyleSlotCssVariable(slotId)] = formatStyleSlotValue(slotId, value);
53
53
  }
54
54
 
55
55
  return variables;
@@ -68,13 +68,13 @@ export function createStyleSlotsCss(slots: StyleSlots, selector = ':root'): stri
68
68
  xl: [],
69
69
  };
70
70
 
71
- for (const slotId of CORE_STYLE_SLOT_IDS) {
71
+ for (const slotId of getRenderableStyleSlotIds(slots)) {
72
72
  const value = slots[slotId];
73
73
  if (value === undefined) {
74
74
  continue;
75
75
  }
76
76
 
77
- const cssVariable = STYLE_SLOT_CSS_VARIABLES[slotId].name;
77
+ const cssVariable = getStyleSlotCssVariable(slotId);
78
78
 
79
79
  if (!isResponsiveRecord(value)) {
80
80
  baseDeclarations.push(`${cssVariable}: ${formatStyleSlotValue(slotId, value)};`);
@@ -114,7 +114,7 @@ export function createStyleSlotsCss(slots: StyleSlots, selector = ':root'): stri
114
114
  }
115
115
 
116
116
  function formatStyleSlotValue(slotId: StyleSlotId, value: unknown): string {
117
- const unit = STYLE_SLOT_CSS_VARIABLES[slotId].unit;
117
+ const unit = getStyleSlotCssVariableConfig(slotId).unit;
118
118
 
119
119
  if (typeof value === 'number') {
120
120
  return unit ? `${value}${unit}` : `${value}`;
@@ -122,3 +122,24 @@ function formatStyleSlotValue(slotId: StyleSlotId, value: unknown): string {
122
122
 
123
123
  return String(value);
124
124
  }
125
+
126
+ function getStyleSlotCssVariableConfig(slotId: StyleSlotId): { name: string; unit?: string } {
127
+ return isCoreStyleSlotId(slotId)
128
+ ? STYLE_SLOT_CSS_VARIABLES[slotId]
129
+ : { name: `--builder-${slotId.split('.').join('-')}` };
130
+ }
131
+
132
+ function getRenderableStyleSlotIds(slots: StyleSlots): StyleSlotId[] {
133
+ const declared = new Set<StyleSlotId>();
134
+ for (const slotId of CORE_STYLE_SLOT_IDS) {
135
+ declared.add(slotId);
136
+ }
137
+ for (const slotId of Object.keys(slots)) {
138
+ declared.add(slotId as StyleSlotId);
139
+ }
140
+ return [...declared];
141
+ }
142
+
143
+ function isCoreStyleSlotId(slotId: StyleSlotId): slotId is CoreStyleSlotId {
144
+ return (CORE_STYLE_SLOT_IDS as readonly string[]).includes(slotId);
145
+ }
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';