@shoppexio/builder-runtime 0.1.1 → 0.1.3
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/YouTubeEmbed.d.ts +13 -0
- package/dist/YouTubeEmbed.d.ts.map +1 -0
- package/dist/YouTubeEmbed.js +49 -0
- package/dist/YouTubeEmbedBuilderBlock.d.ts +7 -0
- package/dist/YouTubeEmbedBuilderBlock.d.ts.map +1 -0
- package/dist/YouTubeEmbedBuilderBlock.js +16 -0
- package/dist/block-style-settings.d.ts +5 -0
- package/dist/block-style-settings.d.ts.map +1 -0
- package/dist/block-style-settings.js +16 -0
- package/dist/builder-runtime.test.d.ts +2 -0
- package/dist/builder-runtime.test.d.ts.map +1 -0
- package/dist/builder-runtime.test.js +115 -0
- package/dist/content.d.ts +6 -0
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +31 -7
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/layout.d.ts +3 -8
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +2 -10
- package/dist/manifest-setting-paths.d.ts +5 -0
- package/dist/manifest-setting-paths.d.ts.map +1 -0
- package/dist/manifest-setting-paths.js +40 -0
- package/dist/merchant-custom-page.d.ts +57 -0
- package/dist/merchant-custom-page.d.ts.map +1 -0
- package/dist/merchant-custom-page.js +63 -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/preview-mode.d.ts +2 -0
- package/dist/preview-mode.d.ts.map +1 -0
- package/dist/preview-mode.js +7 -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-runtime.test.d.ts +2 -0
- package/dist/react-runtime.test.d.ts.map +1 -0
- package/dist/react-runtime.test.js +332 -0
- package/dist/react.d.ts +37 -2
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +138 -46
- 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/dist/youtube-embed-block.d.ts +10 -0
- package/dist/youtube-embed-block.d.ts.map +1 -0
- package/dist/youtube-embed-block.js +19 -0
- package/dist/youtube.d.ts +5 -0
- package/dist/youtube.d.ts.map +1 -0
- package/dist/youtube.js +52 -0
- package/package.json +3 -3
- package/src/YouTubeEmbed.tsx +105 -0
- package/src/YouTubeEmbedBuilderBlock.tsx +49 -0
- package/src/block-style-settings.ts +24 -0
- package/src/builder-runtime.test.ts +69 -0
- package/src/content.ts +44 -9
- package/src/index.ts +8 -0
- package/src/layout.ts +11 -21
- package/src/manifest-setting-paths.test.ts +23 -0
- package/src/manifest-setting-paths.ts +55 -0
- package/src/merchant-custom-page.tsx +161 -0
- package/src/preview-fixtures.ts +56 -0
- package/src/preview-mode.ts +8 -0
- package/src/product-page.test.ts +37 -0
- package/src/product-page.ts +32 -0
- package/src/react-runtime.test.tsx +42 -0
- package/src/react.tsx +243 -49
- 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/src/youtube-embed-block.test.ts +76 -0
- package/src/youtube-embed-block.ts +28 -0
- package/src/youtube-embed-builder-block.test.tsx +166 -0
- package/src/youtube.test.ts +48 -0
- package/src/youtube.ts +66 -0
package/src/react.tsx
CHANGED
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
BuilderSettingsSchema,
|
|
4
4
|
PreviewMessageSchema,
|
|
5
5
|
createEmptyBuilderSettings,
|
|
6
|
+
isTrustedPreviewParentOrigin,
|
|
6
7
|
migrateLegacyBuilderSettings,
|
|
8
|
+
normalizeDedicatedPageId,
|
|
7
9
|
} from '@shoppex/builder-contracts';
|
|
8
10
|
import {
|
|
9
11
|
createElement,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
useMemo,
|
|
14
16
|
useRef,
|
|
15
17
|
useState,
|
|
18
|
+
type ComponentPropsWithoutRef,
|
|
16
19
|
type ComponentType,
|
|
17
20
|
type ElementType,
|
|
18
21
|
type ReactNode,
|
|
@@ -24,8 +27,15 @@ import {
|
|
|
24
27
|
getBuilderContentString,
|
|
25
28
|
getBuilderContentValue,
|
|
26
29
|
} from './content.js';
|
|
30
|
+
import { resolveManifestSettingRecordValue } from './manifest-setting-paths.js';
|
|
31
|
+
import { BUILDER_PREVIEW_REVIEWS } from './preview-fixtures.js';
|
|
27
32
|
import { createBuilderCss } from './css-vars.js';
|
|
33
|
+
import { builderBlock, type BuilderAttributeMap } from './attributes.js';
|
|
28
34
|
import { getPageBlocks, getPageLayout, getVisiblePageBlocks } from './layout.js';
|
|
35
|
+
import {
|
|
36
|
+
getNavigationHeaderSettings,
|
|
37
|
+
resolveSearchBarSettings,
|
|
38
|
+
} from './search-bar-settings.js';
|
|
29
39
|
import { resolveStyleSlotValue } from './style-slots.js';
|
|
30
40
|
|
|
31
41
|
type BuilderRuntimeContextValue = {
|
|
@@ -214,6 +224,7 @@ export function BuilderRuntimePreviewProvider({
|
|
|
214
224
|
message: normalized.message,
|
|
215
225
|
...(normalized.stack ? { stack: normalized.stack } : {}),
|
|
216
226
|
source,
|
|
227
|
+
phase: 'runtime',
|
|
217
228
|
diagnostics: {
|
|
218
229
|
name: normalized.name,
|
|
219
230
|
href: window.location.href,
|
|
@@ -435,15 +446,86 @@ export function useVisibleBuilderPageBlocks(pageId: string) {
|
|
|
435
446
|
|
|
436
447
|
export function useThemePageBlocks(pageId: string, defaultOrder: string[]): BlockInstance[] {
|
|
437
448
|
const { settings } = useBuilderRuntime();
|
|
449
|
+
const canonicalPageId = normalizeDedicatedPageId(pageId);
|
|
438
450
|
|
|
439
451
|
return useMemo(() => {
|
|
440
|
-
const page = settings.theme.layout[
|
|
452
|
+
const page = settings.theme.layout[canonicalPageId];
|
|
441
453
|
if (page) {
|
|
442
454
|
return page.blocks.filter((block) => block.visible);
|
|
443
455
|
}
|
|
444
456
|
|
|
445
457
|
return defaultOrder.map((type) => ({ id: type, type, visible: true, settings: {} }));
|
|
446
|
-
}, [
|
|
458
|
+
}, [canonicalPageId, defaultOrder, settings.theme.layout]);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function useThemePageBlockAttributes(
|
|
462
|
+
pageId: string,
|
|
463
|
+
defaultOrder: string[],
|
|
464
|
+
): BuilderAttributeMap {
|
|
465
|
+
const canonicalPageId = normalizeDedicatedPageId(pageId);
|
|
466
|
+
const blocks = useThemePageBlocks(pageId, defaultOrder);
|
|
467
|
+
const block = blocks[0];
|
|
468
|
+
|
|
469
|
+
return useMemo(
|
|
470
|
+
() => ({
|
|
471
|
+
'data-page-id': canonicalPageId,
|
|
472
|
+
...(block ? builderBlock(block.id, block.type) : {}),
|
|
473
|
+
}),
|
|
474
|
+
[block, canonicalPageId],
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
type DedicatedBuilderPageProps<T extends ElementType> = {
|
|
479
|
+
pageId: string;
|
|
480
|
+
defaultBlockOrder: string[];
|
|
481
|
+
as?: T;
|
|
482
|
+
children: ReactNode;
|
|
483
|
+
} & Omit<ComponentPropsWithoutRef<T>, 'as' | 'children'>;
|
|
484
|
+
|
|
485
|
+
export function DedicatedBuilderPage<T extends ElementType = 'section'>({
|
|
486
|
+
pageId,
|
|
487
|
+
defaultBlockOrder,
|
|
488
|
+
as,
|
|
489
|
+
children,
|
|
490
|
+
...rest
|
|
491
|
+
}: DedicatedBuilderPageProps<T>) {
|
|
492
|
+
const attrs = useThemePageBlockAttributes(pageId, defaultBlockOrder);
|
|
493
|
+
const Component = (as ?? 'section') as ElementType;
|
|
494
|
+
|
|
495
|
+
return createElement(Component, { ...attrs, ...rest }, children);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function useBuilderPreviewReviews<T>(input: {
|
|
499
|
+
reviews: T[];
|
|
500
|
+
isLoading: boolean;
|
|
501
|
+
error: unknown;
|
|
502
|
+
fixtures?: T[];
|
|
503
|
+
}): {
|
|
504
|
+
reviews: T[];
|
|
505
|
+
isLoading: boolean;
|
|
506
|
+
error: unknown;
|
|
507
|
+
isUsingFixtures: boolean;
|
|
508
|
+
} {
|
|
509
|
+
const fixtures = input.fixtures ?? (BUILDER_PREVIEW_REVIEWS as T[]);
|
|
510
|
+
const shouldUseFixtures = isBuilderPreviewRuntime() && Boolean(input.error);
|
|
511
|
+
|
|
512
|
+
return useMemo(() => {
|
|
513
|
+
if (!shouldUseFixtures) {
|
|
514
|
+
return {
|
|
515
|
+
reviews: input.reviews,
|
|
516
|
+
isLoading: input.isLoading,
|
|
517
|
+
error: input.error,
|
|
518
|
+
isUsingFixtures: false,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
reviews: fixtures,
|
|
524
|
+
isLoading: false,
|
|
525
|
+
error: null,
|
|
526
|
+
isUsingFixtures: true,
|
|
527
|
+
};
|
|
528
|
+
}, [fixtures, input.error, input.isLoading, input.reviews, shouldUseFixtures]);
|
|
447
529
|
}
|
|
448
530
|
|
|
449
531
|
export function useBuilderStyleSlot(
|
|
@@ -461,13 +543,17 @@ function useScopedBuilderContentValue(path: string): unknown {
|
|
|
461
543
|
const block = useContext(BuilderBlockContext);
|
|
462
544
|
if (!block) return undefined;
|
|
463
545
|
|
|
464
|
-
|
|
465
|
-
|
|
546
|
+
const resolved = resolveManifestSettingRecordValue(block.settings, path);
|
|
547
|
+
if (resolved !== undefined) {
|
|
548
|
+
return resolved;
|
|
466
549
|
}
|
|
467
550
|
|
|
468
551
|
const shortPath = path.startsWith(`${block.type}.`) ? path.slice(block.type.length + 1) : path.split('.').at(-1);
|
|
469
|
-
if (shortPath
|
|
470
|
-
|
|
552
|
+
if (shortPath) {
|
|
553
|
+
const shortResolved = resolveManifestSettingRecordValue(block.settings, shortPath);
|
|
554
|
+
if (shortResolved !== undefined) {
|
|
555
|
+
return shortResolved;
|
|
556
|
+
}
|
|
471
557
|
}
|
|
472
558
|
|
|
473
559
|
const nested = shortPath ? getNestedBuilderSetting(block.settings, shortPath) : undefined;
|
|
@@ -485,7 +571,60 @@ function getNestedBuilderSetting(record: Record<string, unknown>, path: string):
|
|
|
485
571
|
return current;
|
|
486
572
|
}
|
|
487
573
|
|
|
488
|
-
function
|
|
574
|
+
export function useSearchBarSettings(input: {
|
|
575
|
+
variant: 'hero' | 'navigation';
|
|
576
|
+
headerSettings?: Record<string, unknown>;
|
|
577
|
+
}) {
|
|
578
|
+
const runtime = useBuilderRuntime();
|
|
579
|
+
const headerSettings = useMemo(
|
|
580
|
+
() => input.headerSettings ?? getNavigationHeaderSettings(runtime.settings),
|
|
581
|
+
[input.headerSettings, runtime.settings],
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
const heroPlaceholder = useBuilderContentValue('hero.search.placeholder');
|
|
585
|
+
const heroBackground = useBuilderContentValue('hero.search.background');
|
|
586
|
+
const heroBorderColor = useBuilderContentValue('hero.search.borderColor');
|
|
587
|
+
const heroBorderRadius = useBuilderContentValue('hero.search.borderRadius');
|
|
588
|
+
const heroMaxWidth = useBuilderContentValue('hero.search.maxWidth');
|
|
589
|
+
const heroShowShortcut = useBuilderContentValue('hero.search.showShortcut');
|
|
590
|
+
const heroShow = useBuilderContentValue('hero.search.show');
|
|
591
|
+
|
|
592
|
+
return useMemo(
|
|
593
|
+
() =>
|
|
594
|
+
resolveSearchBarSettings({
|
|
595
|
+
variant: input.variant,
|
|
596
|
+
headerSettings,
|
|
597
|
+
heroValues: {
|
|
598
|
+
'hero.search.placeholder': heroPlaceholder,
|
|
599
|
+
'hero.search.background': heroBackground,
|
|
600
|
+
'hero.search.borderColor': heroBorderColor,
|
|
601
|
+
'hero.search.borderRadius': heroBorderRadius,
|
|
602
|
+
'hero.search.maxWidth': heroMaxWidth,
|
|
603
|
+
'hero.search.showShortcut': heroShowShortcut,
|
|
604
|
+
'hero.search.show': heroShow,
|
|
605
|
+
},
|
|
606
|
+
}),
|
|
607
|
+
[
|
|
608
|
+
headerSettings,
|
|
609
|
+
heroBackground,
|
|
610
|
+
heroBorderColor,
|
|
611
|
+
heroBorderRadius,
|
|
612
|
+
heroMaxWidth,
|
|
613
|
+
heroPlaceholder,
|
|
614
|
+
heroShow,
|
|
615
|
+
heroShowShortcut,
|
|
616
|
+
input.variant,
|
|
617
|
+
],
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export {
|
|
622
|
+
buildSearchShellStyle,
|
|
623
|
+
getNavigationHeaderSettings,
|
|
624
|
+
resolveSearchBarSettings,
|
|
625
|
+
} from './search-bar-settings.js';
|
|
626
|
+
|
|
627
|
+
export function isBuilderPreviewRuntime(): boolean {
|
|
489
628
|
if (typeof window === 'undefined') {
|
|
490
629
|
return false;
|
|
491
630
|
}
|
|
@@ -587,7 +726,7 @@ function getPreviewParentOrigin(location: Location, referrer: string): string |
|
|
|
587
726
|
function isTrustedBuilderPreviewEmbed(location: Location, parentOrigin: string | null): boolean {
|
|
588
727
|
if (window.parent === window || !parentOrigin) return false;
|
|
589
728
|
if (!hasBuilderPreviewMode(location)) return false;
|
|
590
|
-
return
|
|
729
|
+
return isTrustedPreviewParentOrigin(parentOrigin);
|
|
591
730
|
}
|
|
592
731
|
|
|
593
732
|
function hasBuilderPreviewMode(location: Location): boolean {
|
|
@@ -599,25 +738,6 @@ function hasBuilderPreviewMode(location: Location): boolean {
|
|
|
599
738
|
);
|
|
600
739
|
}
|
|
601
740
|
|
|
602
|
-
function isTrustedBuilderPreviewParentOrigin(origin: string): boolean {
|
|
603
|
-
try {
|
|
604
|
-
const parsed = new URL(origin);
|
|
605
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
606
|
-
return (
|
|
607
|
-
hostname === 'dashboard.shoppex.io'
|
|
608
|
-
|| hostname === 'dashboard.shoppex.test'
|
|
609
|
-
|| hostname === 'localhost'
|
|
610
|
-
|| hostname === '127.0.0.1'
|
|
611
|
-
|| hostname === '::1'
|
|
612
|
-
|| hostname.endsWith('.localhost')
|
|
613
|
-
|| hostname.endsWith('.vercel.app')
|
|
614
|
-
|| hostname.endsWith('.vercel.run')
|
|
615
|
-
);
|
|
616
|
-
} catch {
|
|
617
|
-
return false;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
741
|
function selectBuilderElement(blockId: string | undefined): void {
|
|
622
742
|
document
|
|
623
743
|
.querySelectorAll<HTMLElement>(BUILDER_SELECTED_SELECTOR)
|
|
@@ -631,7 +751,10 @@ function selectBuilderElement(blockId: string | undefined): void {
|
|
|
631
751
|
if (!target) return;
|
|
632
752
|
|
|
633
753
|
target.setAttribute('data-builder-selected', 'true');
|
|
634
|
-
target.
|
|
754
|
+
const pageId = target.getAttribute('data-page-id');
|
|
755
|
+
const scrollBlock =
|
|
756
|
+
pageId === 'footer' ? 'end' : pageId === 'navigation' ? 'start' : 'nearest';
|
|
757
|
+
target.scrollIntoView({ behavior: 'smooth', block: scrollBlock });
|
|
635
758
|
}
|
|
636
759
|
|
|
637
760
|
function createBuilderSelection(blockElement: HTMLElement, target: Element): BuilderSelection {
|
|
@@ -785,6 +908,7 @@ function installBuilderPreviewHoverInspector(): () => void {
|
|
|
785
908
|
// we keep a small override table for these.
|
|
786
909
|
const SLUG_LABEL_OVERRIDES: Record<string, string> = {
|
|
787
910
|
'custom-html': 'Custom Embed',
|
|
911
|
+
'youtube-embed': 'YouTube Video',
|
|
788
912
|
};
|
|
789
913
|
const override = SLUG_LABEL_OVERRIDES[type];
|
|
790
914
|
const label = override ?? type
|
|
@@ -825,7 +949,8 @@ function installBuilderPreviewHoverInspector(): () => void {
|
|
|
825
949
|
};
|
|
826
950
|
}
|
|
827
951
|
|
|
828
|
-
const INSERTER_HOVER_BAND_PX =
|
|
952
|
+
const INSERTER_HOVER_BAND_PX = 28;
|
|
953
|
+
const INSERTER_CLEAR_DELAY_MS = 150;
|
|
829
954
|
|
|
830
955
|
/**
|
|
831
956
|
* Direct-manipulation bridge: tracks the bounding rect of the selected
|
|
@@ -924,30 +1049,49 @@ function installBuilderDirectManipulation(
|
|
|
924
1049
|
// a tracked block stack. We don't try to be clever about which gap;
|
|
925
1050
|
// every direct ancestor with a list of `[data-builder-block]` children
|
|
926
1051
|
// contributes one gap per pair.
|
|
1052
|
+
let inserterClearTimer: number | null = null;
|
|
1053
|
+
|
|
927
1054
|
const postInserter = (
|
|
928
1055
|
index: number | null,
|
|
929
1056
|
rect: DOMRect | null,
|
|
930
1057
|
) => {
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
rect
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1058
|
+
const emitInserter = () => {
|
|
1059
|
+
const key =
|
|
1060
|
+
index === null || !rect
|
|
1061
|
+
? '__none__'
|
|
1062
|
+
: `${index}|${rect.top}|${rect.left}|${rect.width}|${rect.height}`;
|
|
1063
|
+
if (key === lastInserterKey) return;
|
|
1064
|
+
lastInserterKey = key;
|
|
1065
|
+
postMessage({
|
|
1066
|
+
type: 'INSERTER_HOVER',
|
|
1067
|
+
revision: getRevision(),
|
|
1068
|
+
index,
|
|
1069
|
+
rect:
|
|
1070
|
+
rect === null
|
|
1071
|
+
? null
|
|
1072
|
+
: {
|
|
1073
|
+
top: rect.top,
|
|
1074
|
+
left: rect.left,
|
|
1075
|
+
width: rect.width,
|
|
1076
|
+
height: rect.height,
|
|
1077
|
+
},
|
|
1078
|
+
});
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
if (index !== null && rect !== null) {
|
|
1082
|
+
if (inserterClearTimer !== null) {
|
|
1083
|
+
window.clearTimeout(inserterClearTimer);
|
|
1084
|
+
inserterClearTimer = null;
|
|
1085
|
+
}
|
|
1086
|
+
emitInserter();
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (inserterClearTimer !== null) return;
|
|
1091
|
+
inserterClearTimer = window.setTimeout(() => {
|
|
1092
|
+
inserterClearTimer = null;
|
|
1093
|
+
emitInserter();
|
|
1094
|
+
}, INSERTER_CLEAR_DELAY_MS);
|
|
951
1095
|
};
|
|
952
1096
|
|
|
953
1097
|
const handleMouseMove = (event: MouseEvent) => {
|
|
@@ -1103,3 +1247,53 @@ function installBuilderDirectManipulation(
|
|
|
1103
1247
|
window.removeEventListener('mousedown', handleMouseDown, true);
|
|
1104
1248
|
};
|
|
1105
1249
|
}
|
|
1250
|
+
|
|
1251
|
+
export {
|
|
1252
|
+
getBuilderBlockSettingText,
|
|
1253
|
+
getBuilderProductBlockAttributes,
|
|
1254
|
+
getLayoutPageBlockAttributes,
|
|
1255
|
+
getProductBlockText,
|
|
1256
|
+
getProductPageBlockAttributes,
|
|
1257
|
+
} from './product-page.js';
|
|
1258
|
+
export {
|
|
1259
|
+
createStandardProductBlockRegistry,
|
|
1260
|
+
splitStandardProductPageBlocks,
|
|
1261
|
+
buildStandardProductInfoTabs,
|
|
1262
|
+
resolveStandardBuyBoxLabels,
|
|
1263
|
+
resolveStandardDetailsLabels,
|
|
1264
|
+
resolveStandardRelatedProductsTitle,
|
|
1265
|
+
resolveScopedBlockSettingText,
|
|
1266
|
+
STANDARD_PRODUCT_BLOCK_TYPES,
|
|
1267
|
+
STANDARD_PRODUCT_PRIMARY_BLOCK_TYPES,
|
|
1268
|
+
STANDARD_PRODUCT_SECONDARY_BLOCK_TYPES,
|
|
1269
|
+
} from './standard-product-blocks.js';
|
|
1270
|
+
export type {
|
|
1271
|
+
StandardBuyBoxLabels,
|
|
1272
|
+
StandardDetailsLabels,
|
|
1273
|
+
StandardProductBlockRegistryData,
|
|
1274
|
+
StandardProductBlockRegistryOptions,
|
|
1275
|
+
StandardProductBlockRegistrySlots,
|
|
1276
|
+
StandardProductBlockType,
|
|
1277
|
+
StandardProductSettingScope,
|
|
1278
|
+
StandardProductTabSpec,
|
|
1279
|
+
} from './standard-product-blocks.js';
|
|
1280
|
+
export { YouTubeEmbed, YouTubeEmbedPreviewPlaceholder, type YouTubeEmbedProps } from './YouTubeEmbed.js';
|
|
1281
|
+
export {
|
|
1282
|
+
YouTubeEmbedBuilderBlock,
|
|
1283
|
+
type YouTubeEmbedBuilderBlockProps,
|
|
1284
|
+
} from './YouTubeEmbedBuilderBlock.js';
|
|
1285
|
+
export {
|
|
1286
|
+
createMerchantCustomPageRegistry,
|
|
1287
|
+
MerchantCustomPageBuilderView,
|
|
1288
|
+
useMerchantCustomPageRegistry,
|
|
1289
|
+
useMerchantCustomPageView,
|
|
1290
|
+
type MerchantCustomPageRegistryOptions,
|
|
1291
|
+
} from './merchant-custom-page.js';
|
|
1292
|
+
export { isBuilderPreviewMode } from './preview-mode.js';
|
|
1293
|
+
export {
|
|
1294
|
+
getYouTubeEmbedBlockStyleProps,
|
|
1295
|
+
readYouTubeEmbedBlockSettings,
|
|
1296
|
+
type YouTubeEmbedBlockInstance,
|
|
1297
|
+
} from './youtube-embed-block.js';
|
|
1298
|
+
export { readManifestStyleBlockProps } from './block-style-settings.js';
|
|
1299
|
+
export { getBuilderPreviewReviewFixtures } from './preview-fixtures.js';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
buildSearchShellStyle,
|
|
4
|
+
resolveSearchBarSettings,
|
|
5
|
+
resolveSearchColor,
|
|
6
|
+
resolveSearchMaxWidth,
|
|
7
|
+
} from './search-bar-settings';
|
|
8
|
+
|
|
9
|
+
describe('resolveSearchBarSettings', () => {
|
|
10
|
+
test('hero variant prefers hero settings and falls back to navigation', () => {
|
|
11
|
+
const resolved = resolveSearchBarSettings({
|
|
12
|
+
variant: 'hero',
|
|
13
|
+
headerSettings: {
|
|
14
|
+
'navigation.search.placeholder': 'Header placeholder',
|
|
15
|
+
'navigation.search.background': '#111111',
|
|
16
|
+
},
|
|
17
|
+
heroValues: {
|
|
18
|
+
'hero.search.placeholder': 'Hero placeholder',
|
|
19
|
+
'hero.search.maxWidth': 'lg',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(resolved.placeholder).toBe('Hero placeholder');
|
|
24
|
+
expect(resolved.backgroundColor).toBe('#111111');
|
|
25
|
+
expect(resolved.maxWidth).toBe('36rem');
|
|
26
|
+
expect(resolved.showShortcut).toBe(true);
|
|
27
|
+
expect(resolved.showInHero).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('navigation variant reads header settings only', () => {
|
|
31
|
+
const resolved = resolveSearchBarSettings({
|
|
32
|
+
variant: 'navigation',
|
|
33
|
+
headerSettings: {
|
|
34
|
+
'navigation.search.placeholder': 'Find products',
|
|
35
|
+
'navigation.search.borderRadius': 24,
|
|
36
|
+
},
|
|
37
|
+
heroValues: {
|
|
38
|
+
'hero.search.placeholder': 'Ignored in navigation mode',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(resolved.placeholder).toBe('Find products');
|
|
43
|
+
expect(resolved.borderRadiusPx).toBe(24);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('search shell helpers', () => {
|
|
48
|
+
test('resolveSearchColor normalizes hex values', () => {
|
|
49
|
+
expect(resolveSearchColor('f97316')).toBe('#f97316');
|
|
50
|
+
expect(resolveSearchColor('not-a-color')).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('resolveSearchMaxWidth maps preset keys', () => {
|
|
54
|
+
expect(resolveSearchMaxWidth('full')).toBe('100%');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('buildSearchShellStyle returns only overridden properties', () => {
|
|
58
|
+
expect(
|
|
59
|
+
buildSearchShellStyle({
|
|
60
|
+
backgroundColor: '#101010',
|
|
61
|
+
borderColor: '#333333',
|
|
62
|
+
borderRadiusPx: 999,
|
|
63
|
+
}),
|
|
64
|
+
).toEqual({
|
|
65
|
+
backgroundColor: '#101010',
|
|
66
|
+
borderColor: '#333333',
|
|
67
|
+
borderWidth: '1px',
|
|
68
|
+
borderStyle: 'solid',
|
|
69
|
+
borderRadius: '999px',
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { BuilderSettings } from '@shoppex/builder-contracts';
|
|
2
|
+
|
|
3
|
+
export const SEARCH_BAR_MAX_WIDTH: Record<string, string> = {
|
|
4
|
+
sm: '20rem',
|
|
5
|
+
md: '28rem',
|
|
6
|
+
lg: '36rem',
|
|
7
|
+
full: '100%',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ResolvedSearchBarSettings = {
|
|
11
|
+
placeholder: string;
|
|
12
|
+
backgroundColor?: string;
|
|
13
|
+
borderColor?: string;
|
|
14
|
+
borderRadiusPx?: number;
|
|
15
|
+
maxWidth: string;
|
|
16
|
+
showShortcut: boolean;
|
|
17
|
+
showInHero: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function getNavigationHeaderSettings(
|
|
21
|
+
settings: BuilderSettings,
|
|
22
|
+
): Record<string, unknown> {
|
|
23
|
+
const block = settings.theme.layout.navigation?.blocks.find(
|
|
24
|
+
(candidate) => candidate.type === 'header',
|
|
25
|
+
);
|
|
26
|
+
return block?.settings ?? {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readSetting(
|
|
30
|
+
sources: Array<Record<string, unknown> | undefined>,
|
|
31
|
+
path: string,
|
|
32
|
+
): unknown {
|
|
33
|
+
for (const source of sources) {
|
|
34
|
+
if (!source) continue;
|
|
35
|
+
if (!Object.prototype.hasOwnProperty.call(source, path)) continue;
|
|
36
|
+
const value = source[path];
|
|
37
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isValidHexColor(value: unknown): value is string {
|
|
45
|
+
if (typeof value !== 'string' || !value.trim()) return false;
|
|
46
|
+
return /^#?[a-f\d]{3}(?:[a-f\d]{3})?(?:[a-f\d]{2})?$/i.test(value.trim());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function normalizeHexColor(value: string): string {
|
|
50
|
+
const trimmed = value.trim();
|
|
51
|
+
return trimmed.startsWith('#') ? trimmed : `#${trimmed}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveSearchColor(value: unknown): string | undefined {
|
|
55
|
+
if (!isValidHexColor(value)) return undefined;
|
|
56
|
+
return normalizeHexColor(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveSearchBorderRadius(value: unknown): number | undefined {
|
|
60
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return undefined;
|
|
61
|
+
return Math.max(0, Math.round(value));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveSearchMaxWidth(value: unknown, fallback = 'md'): string {
|
|
65
|
+
if (typeof value === 'string' && value in SEARCH_BAR_MAX_WIDTH) {
|
|
66
|
+
return SEARCH_BAR_MAX_WIDTH[value]!;
|
|
67
|
+
}
|
|
68
|
+
return SEARCH_BAR_MAX_WIDTH[fallback] ?? SEARCH_BAR_MAX_WIDTH.md!;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveSearchBoolean(value: unknown, fallback: boolean): boolean {
|
|
72
|
+
if (typeof value === 'boolean') return value;
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveSearchPlaceholder(
|
|
77
|
+
value: unknown,
|
|
78
|
+
fallback: string,
|
|
79
|
+
): string {
|
|
80
|
+
return typeof value === 'string' && value.trim() ? value : fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveSearchBarSettings(input: {
|
|
84
|
+
variant: 'hero' | 'navigation';
|
|
85
|
+
heroValues: Record<string, unknown>;
|
|
86
|
+
headerSettings: Record<string, unknown>;
|
|
87
|
+
}): ResolvedSearchBarSettings {
|
|
88
|
+
const navSources = [input.headerSettings];
|
|
89
|
+
const heroSources = [input.heroValues, input.headerSettings];
|
|
90
|
+
|
|
91
|
+
if (input.variant === 'hero') {
|
|
92
|
+
return {
|
|
93
|
+
placeholder: resolveSearchPlaceholder(
|
|
94
|
+
readSetting(heroSources, 'hero.search.placeholder') ??
|
|
95
|
+
readSetting(navSources, 'navigation.search.placeholder'),
|
|
96
|
+
'Search products…',
|
|
97
|
+
),
|
|
98
|
+
backgroundColor: resolveSearchColor(
|
|
99
|
+
readSetting(heroSources, 'hero.search.background') ??
|
|
100
|
+
readSetting(navSources, 'navigation.search.background'),
|
|
101
|
+
),
|
|
102
|
+
borderColor: resolveSearchColor(
|
|
103
|
+
readSetting(heroSources, 'hero.search.borderColor') ??
|
|
104
|
+
readSetting(navSources, 'navigation.search.borderColor'),
|
|
105
|
+
),
|
|
106
|
+
borderRadiusPx: resolveSearchBorderRadius(
|
|
107
|
+
readSetting(heroSources, 'hero.search.borderRadius') ??
|
|
108
|
+
readSetting(navSources, 'navigation.search.borderRadius'),
|
|
109
|
+
),
|
|
110
|
+
maxWidth: resolveSearchMaxWidth(
|
|
111
|
+
readSetting(heroSources, 'hero.search.maxWidth'),
|
|
112
|
+
'md',
|
|
113
|
+
),
|
|
114
|
+
showShortcut: resolveSearchBoolean(
|
|
115
|
+
readSetting(heroSources, 'hero.search.showShortcut'),
|
|
116
|
+
true,
|
|
117
|
+
),
|
|
118
|
+
showInHero: resolveSearchBoolean(
|
|
119
|
+
readSetting(heroSources, 'hero.search.show'),
|
|
120
|
+
true,
|
|
121
|
+
),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
placeholder: resolveSearchPlaceholder(
|
|
127
|
+
readSetting(navSources, 'navigation.search.placeholder'),
|
|
128
|
+
'Search products…',
|
|
129
|
+
),
|
|
130
|
+
backgroundColor: resolveSearchColor(
|
|
131
|
+
readSetting(navSources, 'navigation.search.background'),
|
|
132
|
+
),
|
|
133
|
+
borderColor: resolveSearchColor(
|
|
134
|
+
readSetting(navSources, 'navigation.search.borderColor'),
|
|
135
|
+
),
|
|
136
|
+
borderRadiusPx: resolveSearchBorderRadius(
|
|
137
|
+
readSetting(navSources, 'navigation.search.borderRadius'),
|
|
138
|
+
),
|
|
139
|
+
maxWidth: SEARCH_BAR_MAX_WIDTH.md!,
|
|
140
|
+
showShortcut: true,
|
|
141
|
+
showInHero: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildSearchShellStyle(
|
|
146
|
+
settings: Pick<
|
|
147
|
+
ResolvedSearchBarSettings,
|
|
148
|
+
'backgroundColor' | 'borderColor' | 'borderRadiusPx'
|
|
149
|
+
>,
|
|
150
|
+
): {
|
|
151
|
+
backgroundColor?: string;
|
|
152
|
+
borderColor?: string;
|
|
153
|
+
borderWidth?: string;
|
|
154
|
+
borderStyle?: 'solid';
|
|
155
|
+
borderRadius?: string;
|
|
156
|
+
} | undefined {
|
|
157
|
+
const style: {
|
|
158
|
+
backgroundColor?: string;
|
|
159
|
+
borderColor?: string;
|
|
160
|
+
borderWidth?: string;
|
|
161
|
+
borderStyle?: 'solid';
|
|
162
|
+
borderRadius?: string;
|
|
163
|
+
} = {};
|
|
164
|
+
if (settings.backgroundColor) {
|
|
165
|
+
style.backgroundColor = settings.backgroundColor;
|
|
166
|
+
}
|
|
167
|
+
if (settings.borderColor) {
|
|
168
|
+
style.borderColor = settings.borderColor;
|
|
169
|
+
style.borderWidth = '1px';
|
|
170
|
+
style.borderStyle = 'solid';
|
|
171
|
+
}
|
|
172
|
+
if (settings.borderRadiusPx !== undefined) {
|
|
173
|
+
style.borderRadius = `${settings.borderRadiusPx}px`;
|
|
174
|
+
}
|
|
175
|
+
return Object.keys(style).length > 0 ? style : undefined;
|
|
176
|
+
}
|