@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.
- package/dist/css-vars.d.ts.map +1 -1
- package/dist/css-vars.js +24 -6
- 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 +5 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +2 -0
- 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 +36 -224
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +606 -47
- 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 +57 -0
- package/src/css-vars.ts +29 -8
- package/src/index.ts +4 -0
- package/src/layout.ts +14 -1
- 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 +215 -3
- package/src/react.tsx +769 -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
- package/dist/builder-runtime.test.d.ts +0 -2
- package/dist/builder-runtime.test.d.ts.map +0 -1
- package/dist/builder-runtime.test.js +0 -115
- package/dist/react-runtime.test.d.ts +0 -2
- package/dist/react-runtime.test.d.ts.map +0 -1
- 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 @@
|
|
|
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",
|
|
@@ -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<
|
|
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
|
|
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
|
|
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[
|
|
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
|
|
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 =
|
|
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 =
|
|
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';
|