@shoppexio/builder-runtime 0.1.2 → 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.
Files changed (56) hide show
  1. package/dist/YouTubeEmbed.d.ts +13 -0
  2. package/dist/YouTubeEmbed.d.ts.map +1 -0
  3. package/dist/YouTubeEmbed.js +49 -0
  4. package/dist/YouTubeEmbedBuilderBlock.d.ts +7 -0
  5. package/dist/YouTubeEmbedBuilderBlock.d.ts.map +1 -0
  6. package/dist/YouTubeEmbedBuilderBlock.js +16 -0
  7. package/dist/block-style-settings.d.ts +5 -0
  8. package/dist/block-style-settings.d.ts.map +1 -0
  9. package/dist/block-style-settings.js +16 -0
  10. package/dist/builder-runtime.test.d.ts +2 -0
  11. package/dist/builder-runtime.test.d.ts.map +1 -0
  12. package/dist/builder-runtime.test.js +115 -0
  13. package/dist/content.d.ts +6 -0
  14. package/dist/content.d.ts.map +1 -1
  15. package/dist/content.js +31 -7
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +4 -0
  19. package/dist/manifest-setting-paths.d.ts +5 -0
  20. package/dist/manifest-setting-paths.d.ts.map +1 -0
  21. package/dist/manifest-setting-paths.js +40 -0
  22. package/dist/merchant-custom-page.d.ts +57 -0
  23. package/dist/merchant-custom-page.d.ts.map +1 -0
  24. package/dist/merchant-custom-page.js +63 -0
  25. package/dist/preview-mode.d.ts +2 -0
  26. package/dist/preview-mode.d.ts.map +1 -0
  27. package/dist/preview-mode.js +7 -0
  28. package/dist/react-runtime.test.d.ts +2 -0
  29. package/dist/react-runtime.test.d.ts.map +1 -0
  30. package/dist/react-runtime.test.js +332 -0
  31. package/dist/react.d.ts +6 -0
  32. package/dist/react.d.ts.map +1 -1
  33. package/dist/react.js +16 -4
  34. package/dist/youtube-embed-block.d.ts +10 -0
  35. package/dist/youtube-embed-block.d.ts.map +1 -0
  36. package/dist/youtube-embed-block.js +19 -0
  37. package/dist/youtube.d.ts +5 -0
  38. package/dist/youtube.d.ts.map +1 -0
  39. package/dist/youtube.js +52 -0
  40. package/package.json +1 -1
  41. package/src/YouTubeEmbed.tsx +105 -0
  42. package/src/YouTubeEmbedBuilderBlock.tsx +49 -0
  43. package/src/block-style-settings.ts +24 -0
  44. package/src/builder-runtime.test.ts +36 -0
  45. package/src/content.ts +44 -9
  46. package/src/index.ts +4 -0
  47. package/src/manifest-setting-paths.test.ts +23 -0
  48. package/src/manifest-setting-paths.ts +55 -0
  49. package/src/merchant-custom-page.tsx +161 -0
  50. package/src/preview-mode.ts +8 -0
  51. package/src/react.tsx +29 -4
  52. package/src/youtube-embed-block.test.ts +76 -0
  53. package/src/youtube-embed-block.ts +28 -0
  54. package/src/youtube-embed-builder-block.test.tsx +166 -0
  55. package/src/youtube.test.ts +48 -0
  56. package/src/youtube.ts +66 -0
@@ -0,0 +1,13 @@
1
+ export interface YouTubeEmbedProps {
2
+ videoUrl?: string;
3
+ title?: string;
4
+ height?: number;
5
+ privacyEnhanced?: boolean;
6
+ className?: string;
7
+ }
8
+ export declare function YouTubeEmbed({ videoUrl, title, height, privacyEnhanced, className, }: YouTubeEmbedProps): import("react/jsx-runtime").JSX.Element | null;
9
+ export declare function YouTubeEmbedPreviewPlaceholder({ className, height, }: {
10
+ className?: string;
11
+ height?: number;
12
+ }): import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=YouTubeEmbed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YouTubeEmbed.d.ts","sourceRoot":"","sources":["../src/YouTubeEmbed.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD,wBAAgB,YAAY,CAAC,EAC3B,QAAa,EACb,KAAuB,EACvB,MAAM,EACN,eAAuB,EACvB,SAAc,GACf,EAAE,iBAAiB,kDAoCnB;AAED,wBAAgB,8BAA8B,CAAC,EAC7C,SAAc,EACd,MAAM,GACP,EAAE;IACD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,2CAsCA"}
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useMemo } from 'react';
4
+ import { buildYouTubeEmbedSrc, parseYouTubeVideoId } from './youtube.js';
5
+ const DEFAULT_HEIGHT = 400;
6
+ const EMBED_MAX_WIDTH = 640;
7
+ export function YouTubeEmbed({ videoUrl = '', title = 'YouTube video', height, privacyEnhanced = false, className = '', }) {
8
+ const videoId = useMemo(() => parseYouTubeVideoId(videoUrl), [videoUrl]);
9
+ const resolvedHeight = typeof height === 'number' && height > 0 ? height : DEFAULT_HEIGHT;
10
+ if (!videoId) {
11
+ return null;
12
+ }
13
+ return (_jsx("section", { className: `my-8 ${className}`, style: { display: 'flex', justifyContent: 'center' }, children: _jsx("div", { style: {
14
+ position: 'relative',
15
+ width: '100%',
16
+ maxWidth: `${EMBED_MAX_WIDTH}px`,
17
+ height: `${resolvedHeight}px`,
18
+ borderRadius: 'inherit',
19
+ }, children: _jsx("iframe", { src: buildYouTubeEmbedSrc(videoId, { privacyEnhanced }), title: title, allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true, loading: "lazy", style: {
20
+ display: 'block',
21
+ width: '100%',
22
+ height: '100%',
23
+ border: 0,
24
+ } }) }) }));
25
+ }
26
+ export function YouTubeEmbedPreviewPlaceholder({ className = '', height, }) {
27
+ const resolvedHeight = typeof height === 'number' && height > 0 ? height : DEFAULT_HEIGHT;
28
+ return (_jsx("section", { className: `my-8 ${className}`, style: { display: 'flex', justifyContent: 'center' }, children: _jsx("div", { style: {
29
+ position: 'relative',
30
+ width: '100%',
31
+ maxWidth: `${EMBED_MAX_WIDTH}px`,
32
+ height: `${resolvedHeight}px`,
33
+ borderRadius: 'inherit',
34
+ }, children: _jsx("div", { style: {
35
+ display: 'flex',
36
+ alignItems: 'center',
37
+ justifyContent: 'center',
38
+ width: '100%',
39
+ height: '100%',
40
+ padding: '0 24px',
41
+ border: '1px dashed rgba(127, 127, 127, 0.35)',
42
+ borderRadius: 'inherit',
43
+ background: 'rgba(127, 127, 127, 0.06)',
44
+ color: '#5f6470',
45
+ fontFamily: 'Inter, system-ui, sans-serif',
46
+ fontSize: '13px',
47
+ textAlign: 'center',
48
+ }, "aria-hidden": "true", children: "Paste a YouTube link or video ID in the Inspector to preview the player here." }) }) }));
49
+ }
@@ -0,0 +1,7 @@
1
+ import type { BlockInstance } from '@shoppex/builder-contracts';
2
+ export type YouTubeEmbedBuilderBlockProps = {
3
+ block: Pick<BlockInstance, 'id' | 'type' | 'settings'>;
4
+ pageId?: string;
5
+ };
6
+ export declare function YouTubeEmbedBuilderBlock({ block, pageId, }: YouTubeEmbedBuilderBlockProps): import("react/jsx-runtime").JSX.Element | null;
7
+ //# sourceMappingURL=YouTubeEmbedBuilderBlock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YouTubeEmbedBuilderBlock.d.ts","sourceRoot":"","sources":["../src/YouTubeEmbedBuilderBlock.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAUhE,MAAM,MAAM,6BAA6B,GAAG;IAC1C,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,MAAM,GAAG,UAAU,CAAC,CAAC;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,EACvC,KAAK,EACL,MAAe,GAChB,EAAE,6BAA6B,kDA8B/B"}
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { builderBlock } from './attributes.js';
3
+ import { isBuilderPreviewMode } from './preview-mode.js';
4
+ import { YouTubeEmbed, YouTubeEmbedPreviewPlaceholder } from './YouTubeEmbed.js';
5
+ import { getYouTubeEmbedBlockStyleProps, readYouTubeEmbedBlockSettings, } from './youtube-embed-block.js';
6
+ import { parseYouTubeVideoId } from './youtube.js';
7
+ export function YouTubeEmbedBuilderBlock({ block, pageId = 'home', }) {
8
+ const settings = readYouTubeEmbedBlockSettings(block);
9
+ const videoId = parseYouTubeVideoId(settings.videoUrl);
10
+ const showPreviewPlaceholder = !videoId && isBuilderPreviewMode();
11
+ if (!videoId && !showPreviewPlaceholder) {
12
+ return null;
13
+ }
14
+ const styleProps = getYouTubeEmbedBlockStyleProps(block);
15
+ return (_jsx("div", { "data-page-id": pageId, ...builderBlock(block.id, block.type), style: Object.keys(styleProps).length > 0 ? styleProps : undefined, children: videoId ? (_jsx(YouTubeEmbed, { videoUrl: settings.videoUrl, title: settings.title, height: settings.height, privacyEnhanced: settings.privacyEnhanced })) : (_jsx(YouTubeEmbedPreviewPlaceholder, { height: settings.height })) }, block.id));
16
+ }
@@ -0,0 +1,5 @@
1
+ import type { CSSProperties } from 'react';
2
+ type StyleBlockSettings = Record<string, unknown>;
3
+ export declare function readManifestStyleBlockProps(settings: StyleBlockSettings): CSSProperties;
4
+ export {};
5
+ //# sourceMappingURL=block-style-settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-style-settings.d.ts","sourceRoot":"","sources":["../src/block-style-settings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,KAAK,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElD,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,kBAAkB,GAC3B,aAAa,CAiBf"}
@@ -0,0 +1,16 @@
1
+ export function readManifestStyleBlockProps(settings) {
2
+ const bg = settings['style.background'];
3
+ const radius = settings['style.borderRadius'];
4
+ const padding = settings['style.padding'];
5
+ const style = {};
6
+ if (typeof bg === 'string' && bg.length > 0) {
7
+ style.backgroundColor = bg;
8
+ }
9
+ if (typeof radius === 'number') {
10
+ style.borderRadius = `${radius}px`;
11
+ }
12
+ if (typeof padding === 'number') {
13
+ style.padding = `${padding}px`;
14
+ }
15
+ return style;
16
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=builder-runtime.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builder-runtime.test.d.ts","sourceRoot":"","sources":["../src/builder-runtime.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,115 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { createBlockInstance, createEmptyBuilderSettings } from '@shoppex/builder-contracts';
3
+ import { builderBlock, builderContent, builderSlot, canAddBlock, createBuilderCss, getBuilderContentList, getBuilderContentString, getPageBlocks, resolveBlockSettings, resolveStyleSlotValue, } from './index.js';
4
+ function createSettings() {
5
+ return {
6
+ ...createEmptyBuilderSettings(1),
7
+ theme: {
8
+ content: {
9
+ 'hero.title': 'Launch sale',
10
+ faq: {
11
+ items: [{ question: 'Can I edit sections?', answer: 'Yes' }],
12
+ },
13
+ },
14
+ layout: {
15
+ home: {
16
+ blocks: [
17
+ createBlockInstance({
18
+ id: 'hero-1',
19
+ type: 'hero',
20
+ settings: { title: 'Hero block' },
21
+ style_overrides: {
22
+ 'button.radius': { base: 14 },
23
+ },
24
+ }),
25
+ ],
26
+ },
27
+ },
28
+ style_slots: {
29
+ 'button.radius': { base: 8, md: 12 },
30
+ 'color.primary': '#ff5500',
31
+ },
32
+ pages: [],
33
+ terms: {},
34
+ },
35
+ };
36
+ }
37
+ const manifest = {
38
+ id: 'default',
39
+ name: 'Default',
40
+ version: '2.0.0',
41
+ pages: {
42
+ home: {
43
+ label: 'Home',
44
+ allowedBlocks: ['hero'],
45
+ defaultBlocks: [],
46
+ },
47
+ },
48
+ blocks: {
49
+ hero: {
50
+ label: 'Hero',
51
+ variants: [],
52
+ settings: {
53
+ title: { type: 'text', label: 'Headline', defaultValue: 'Default hero' },
54
+ },
55
+ exposedStyleSlots: ['button.radius'],
56
+ presets: [],
57
+ },
58
+ },
59
+ styleSlots: {},
60
+ presets: {},
61
+ };
62
+ describe('@shoppex/builder-runtime', () => {
63
+ test('reads direct and nested content values', () => {
64
+ const settings = createSettings();
65
+ expect(getBuilderContentString(settings, 'hero.title')).toBe('Launch sale');
66
+ expect(getBuilderContentList(settings, 'faq.items')).toEqual([{ question: 'Can I edit sections?', answer: 'Yes' }]);
67
+ });
68
+ test('preserves intentionally empty content strings', () => {
69
+ const settings = createSettings();
70
+ settings.theme.content['hero.subtitle'] = '';
71
+ expect(getBuilderContentString(settings, 'hero.subtitle', 'Default subtitle')).toBe('');
72
+ });
73
+ test('resolves page blocks', () => {
74
+ const settings = createSettings();
75
+ expect(getPageBlocks(settings, 'home')).toHaveLength(1);
76
+ expect(getPageBlocks(settings, 'missing')).toEqual([]);
77
+ });
78
+ test('resolves style slots with breakpoint fallback and block override', () => {
79
+ const settings = createSettings();
80
+ const block = settings.theme.layout.home.blocks[0];
81
+ expect(resolveStyleSlotValue(settings, 'button.radius', { breakpoint: 'lg' })).toBe(12);
82
+ expect(resolveStyleSlotValue(settings, 'button.radius', { block, breakpoint: 'md' })).toBe(14);
83
+ });
84
+ test('emits CSS variables with responsive media blocks', () => {
85
+ const css = createBuilderCss(createSettings());
86
+ expect(css).toContain('--builder-button-radius: 8px;');
87
+ expect(css).toContain('--builder-color-primary: #ff5500;');
88
+ expect(css).toContain('@media (min-width: 768px)');
89
+ expect(css).toContain('--builder-button-radius: 12px;');
90
+ });
91
+ test('checks block limits against the manifest', () => {
92
+ const settings = createSettings();
93
+ expect(canAddBlock(settings, manifest, 'home', 'hero')).toBe(true);
94
+ expect(canAddBlock(settings, manifest, 'home', 'faq')).toBe(false);
95
+ });
96
+ test('merges block defaults from the manifest', () => {
97
+ const block = createBlockInstance({
98
+ id: 'hero-2',
99
+ type: 'hero',
100
+ settings: {},
101
+ });
102
+ expect(resolveBlockSettings(block, manifest)).toEqual({ title: 'Default hero' });
103
+ });
104
+ test('creates the three supported builder attributes', () => {
105
+ expect(builderContent('hero.title')).toEqual({ 'data-builder-content': 'hero.title' });
106
+ expect(builderSlot('button.radius', { blockId: 'hero-1' })).toEqual({
107
+ 'data-builder-slot': 'button.radius',
108
+ 'data-builder-block': 'hero-1',
109
+ });
110
+ expect(builderBlock('hero-1', 'hero')).toEqual({
111
+ 'data-builder-block': 'hero-1',
112
+ 'data-builder-block-type': 'hero',
113
+ });
114
+ });
115
+ });
package/dist/content.d.ts CHANGED
@@ -3,6 +3,12 @@ export type JsonRecord = Record<string, unknown>;
3
3
  export declare function getBuilderContentValue(settings: BuilderSettings, path: string): unknown;
4
4
  export declare function getBuilderContentString(settings: BuilderSettings, path: string, fallback?: string): string | undefined;
5
5
  export declare function getBuilderContentList<T = unknown>(settings: BuilderSettings, path: string, fallback?: T[]): T[];
6
+ export type NormalizedGalleryItem = {
7
+ url: string;
8
+ alt?: string;
9
+ caption?: string;
10
+ };
11
+ export declare function normalizeGalleryItems(raw: unknown): NormalizedGalleryItem[];
6
12
  export declare function getBuilderContentRecord(settings: BuilderSettings): JsonRecord;
7
13
  export declare function getBlockSettingValue<T = unknown>(settings: BuilderSettings, input: {
8
14
  pageId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAQvF;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGtH;AAED,wBAAgB,qBAAqB,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,CAAC,EAAO,GAAG,CAAC,EAAE,CAGnH;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,UAAU,CAE7E;AAED,wBAAgB,oBAAoB,CAAC,CAAC,GAAG,OAAO,EAC9C,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,CAAC,CAAA;CAAE,GACpE,CAAC,CAYH"}
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGlE,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvF;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGtH;AAED,wBAAgB,qBAAqB,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,CAAC,EAAO,GAAG,CAAC,EAAE,CAGnH;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,qBAAqB,EAAE,CA+B3E;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,UAAU,CAE7E;AAED,wBAAgB,oBAAoB,CAAC,CAAC,GAAG,OAAO,EAC9C,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,CAAC,CAAA;CAAE,GACpE,CAAC,CAaH"}
package/dist/content.js CHANGED
@@ -1,9 +1,6 @@
1
+ import { resolveManifestSettingRecordValue } from './manifest-setting-paths.js';
1
2
  export function getBuilderContentValue(settings, path) {
2
- const content = settings.theme.content;
3
- if (Object.prototype.hasOwnProperty.call(content, path)) {
4
- return content[path];
5
- }
6
- return getByDottedPath(content, path);
3
+ return resolveManifestSettingRecordValue(settings.theme.content, path);
7
4
  }
8
5
  export function getBuilderContentString(settings, path, fallback) {
9
6
  const value = getBuilderContentValue(settings, path);
@@ -13,6 +10,32 @@ export function getBuilderContentList(settings, path, fallback = []) {
13
10
  const value = getBuilderContentValue(settings, path);
14
11
  return Array.isArray(value) ? value : fallback;
15
12
  }
13
+ export function normalizeGalleryItems(raw) {
14
+ if (!Array.isArray(raw)) {
15
+ return [];
16
+ }
17
+ const items = [];
18
+ for (const entry of raw) {
19
+ if (typeof entry !== 'object' || entry === null) {
20
+ continue;
21
+ }
22
+ const record = entry;
23
+ const url = typeof record.url === 'string' && record.url.length > 0
24
+ ? record.url
25
+ : typeof record.image === 'string' && record.image.length > 0
26
+ ? record.image
27
+ : null;
28
+ if (!url) {
29
+ continue;
30
+ }
31
+ items.push({
32
+ url,
33
+ alt: typeof record.alt === 'string' ? record.alt : undefined,
34
+ caption: typeof record.caption === 'string' ? record.caption : undefined,
35
+ });
36
+ }
37
+ return items;
38
+ }
16
39
  export function getBuilderContentRecord(settings) {
17
40
  return settings.theme.content;
18
41
  }
@@ -21,8 +44,9 @@ export function getBlockSettingValue(settings, input) {
21
44
  if (!block) {
22
45
  return input.fallback;
23
46
  }
24
- if (Object.prototype.hasOwnProperty.call(block.settings, input.path)) {
25
- return block.settings[input.path];
47
+ const resolved = resolveManifestSettingRecordValue(block.settings, input.path);
48
+ if (resolved !== undefined) {
49
+ return resolved;
26
50
  }
27
51
  const nested = getByDottedPath(block.settings, input.path);
28
52
  return nested === undefined ? input.fallback : nested;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './manifest-setting-paths.js';
1
2
  export * from './preview-fixtures.js';
2
3
  export * from './product-page.js';
3
4
  export * from './standard-product-page.js';
@@ -8,4 +9,7 @@ export * from './layout.js';
8
9
  export * from './react.js';
9
10
  export * from './storefront-google-fonts.js';
10
11
  export * from './style-slots.js';
12
+ export * from './block-style-settings.js';
13
+ export * from './preview-mode.js';
14
+ export * from './youtube.js';
11
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kBAAkB,CAAC;AACjC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './manifest-setting-paths.js';
1
2
  export * from './preview-fixtures.js';
2
3
  export * from './product-page.js';
3
4
  export * from './standard-product-page.js';
@@ -8,3 +9,6 @@ export * from './layout.js';
8
9
  export * from './react.js';
9
10
  export * from './storefront-google-fonts.js';
10
11
  export * from './style-slots.js';
12
+ export * from './block-style-settings.js';
13
+ export * from './preview-mode.js';
14
+ export * from './youtube.js';
@@ -0,0 +1,5 @@
1
+ import type { JsonRecord } from './content.js';
2
+ export declare function toSnakeCaseManifestSegment(value: string): string;
3
+ export declare function toSnakeCaseManifestPath(path: string): string;
4
+ export declare function resolveManifestSettingRecordValue(record: JsonRecord, path: string): unknown;
5
+ //# sourceMappingURL=manifest-setting-paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-setting-paths.d.ts","sourceRoot":"","sources":["../src/manifest-setting-paths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM5D;AAgBD,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAoBT"}
@@ -0,0 +1,40 @@
1
+ export function toSnakeCaseManifestSegment(value) {
2
+ return value
3
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
4
+ .replace(/[\s-]+/g, '_')
5
+ .toLowerCase();
6
+ }
7
+ export function toSnakeCaseManifestPath(path) {
8
+ return path
9
+ .split('.')
10
+ .filter(Boolean)
11
+ .map(toSnakeCaseManifestSegment)
12
+ .join('.');
13
+ }
14
+ function getByDottedPath(record, path) {
15
+ let current = record;
16
+ for (const segment of path.split('.')) {
17
+ if (!current || typeof current !== 'object') {
18
+ return undefined;
19
+ }
20
+ current = current[segment];
21
+ }
22
+ return current;
23
+ }
24
+ export function resolveManifestSettingRecordValue(record, path) {
25
+ if (Object.prototype.hasOwnProperty.call(record, path)) {
26
+ return record[path];
27
+ }
28
+ const snakePath = toSnakeCaseManifestPath(path);
29
+ if (snakePath !== path && Object.prototype.hasOwnProperty.call(record, snakePath)) {
30
+ return record[snakePath];
31
+ }
32
+ const nested = getByDottedPath(record, path);
33
+ if (nested !== undefined) {
34
+ return nested;
35
+ }
36
+ if (snakePath !== path) {
37
+ return getByDottedPath(record, snakePath);
38
+ }
39
+ return undefined;
40
+ }
@@ -0,0 +1,57 @@
1
+ import type { ComponentType, ReactNode } from 'react';
2
+ import { type BuilderBlockRegistry } from './react.js';
3
+ type CustomEmbedProps = {
4
+ embedHtml?: string;
5
+ height?: number;
6
+ autoResize?: boolean;
7
+ width?: 'full' | 'boxed' | 'embed';
8
+ };
9
+ type TextBlockProps = {
10
+ eyebrow?: string;
11
+ title?: string;
12
+ body?: string;
13
+ alignment?: 'left' | 'center' | 'right';
14
+ ctaLabel?: string;
15
+ ctaHref?: string;
16
+ styleBackground?: string;
17
+ styleAccentColor?: string;
18
+ };
19
+ export type MerchantCustomPageRegistryOptions = {
20
+ pageId: string;
21
+ CustomEmbed: ComponentType<CustomEmbedProps>;
22
+ TextBlock?: ComponentType<TextBlockProps>;
23
+ };
24
+ export declare function createMerchantCustomPageRegistry(options: MerchantCustomPageRegistryOptions): BuilderBlockRegistry;
25
+ export declare function MerchantCustomPageBuilderView({ pageId, registry, className, children, }: {
26
+ pageId: string;
27
+ registry: BuilderBlockRegistry;
28
+ className?: string;
29
+ children?: ReactNode;
30
+ }): import("react/jsx-runtime").JSX.Element | null;
31
+ export declare function useMerchantCustomPageRegistry(options: MerchantCustomPageRegistryOptions | null): BuilderBlockRegistry | null;
32
+ export declare function useMerchantCustomPageView(slug: string | undefined, components: Pick<MerchantCustomPageRegistryOptions, 'CustomEmbed' | 'TextBlock'>): {
33
+ customPage: {
34
+ id: string;
35
+ title: string;
36
+ slug: string;
37
+ visible: boolean;
38
+ layout: {
39
+ blocks: {
40
+ id: string;
41
+ type: string;
42
+ visible: boolean;
43
+ settings: Record<string, unknown>;
44
+ variant?: string | undefined;
45
+ style_overrides?: Partial<Record<import("@shoppex/builder-contracts").StyleSlotId, import("@shoppex/builder-contracts").StyleSlotValue>> | undefined;
46
+ }[];
47
+ };
48
+ seo?: {
49
+ title?: string | undefined;
50
+ description?: string | undefined;
51
+ } | undefined;
52
+ } | undefined;
53
+ registry: BuilderBlockRegistry | null;
54
+ hasBuilderContent: boolean;
55
+ };
56
+ export {};
57
+ //# sourceMappingURL=merchant-custom-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merchant-custom-page.d.ts","sourceRoot":"","sources":["../src/merchant-custom-page.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAItD,OAAO,EAAsD,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAG3G,KAAK,gBAAgB,GAAG;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;CACpC,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC7C,SAAS,CAAC,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;CAC3C,CAAC;AAYF,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,oBAAoB,CAqDtB;AAED,wBAAgB,6BAA6B,CAAC,EAC5C,MAAM,EACN,QAAQ,EACR,SAAS,EACT,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,kDAaA;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,iCAAiC,GAAG,IAAI,GAChD,oBAAoB,GAAG,IAAI,CAK7B;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,UAAU,EAAE,IAAI,CAAC,iCAAiC,EAAE,aAAa,GAAG,WAAW,CAAC;;;;;;;;;;;;;;;;;;;;;;;EAsBjF"}
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { findCustomPageBySlug } from '@shoppex/builder-contracts';
4
+ import { useMemo } from 'react';
5
+ import { builderBlock } from './attributes.js';
6
+ import { readManifestStyleBlockProps } from './block-style-settings.js';
7
+ import { BuilderPage, useBuilderRuntime, useThemePageBlocks } from './react.js';
8
+ import { YouTubeEmbedBuilderBlock } from './YouTubeEmbedBuilderBlock.js';
9
+ function readStringSetting(block, key) {
10
+ const value = block.settings[key];
11
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
12
+ }
13
+ function readNumberSetting(block, key) {
14
+ const value = block.settings[key];
15
+ return typeof value === 'number' && value > 0 ? value : undefined;
16
+ }
17
+ export function createMerchantCustomPageRegistry(options) {
18
+ const { pageId, CustomEmbed, TextBlock } = options;
19
+ const registry = {
20
+ 'youtube-embed': ({ block }) => (_jsx(YouTubeEmbedBuilderBlock, { block: block, pageId: pageId })),
21
+ 'custom-html': ({ block }) => {
22
+ const styleProps = readManifestStyleBlockProps(block.settings);
23
+ const width = block.settings.width;
24
+ return (_jsx("div", { "data-page-id": pageId, ...builderBlock(block.id, block.type), style: Object.keys(styleProps).length > 0 ? styleProps : undefined, children: _jsx(CustomEmbed, { embedHtml: readStringSetting(block, 'embedHtml'), height: readNumberSetting(block, 'height'), autoResize: block.settings.autoResize === true, width: width === 'full' || width === 'boxed' || width === 'embed'
25
+ ? width
26
+ : 'embed' }) }, block.id));
27
+ },
28
+ };
29
+ if (TextBlock) {
30
+ registry['text-block'] = ({ block }) => (_jsx("div", { "data-page-id": pageId, ...builderBlock(block.id, block.type), children: _jsx(TextBlock, { eyebrow: readStringSetting(block, 'eyebrow'), title: readStringSetting(block, 'title'), body: readStringSetting(block, 'body'), alignment: readStringSetting(block, 'alignment') ??
31
+ undefined, ctaLabel: readStringSetting(block, 'ctaLabel'), ctaHref: readStringSetting(block, 'ctaHref'), styleBackground: readStringSetting(block, 'style.background'), styleAccentColor: readStringSetting(block, 'style.accentColor') }) }, block.id));
32
+ }
33
+ return registry;
34
+ }
35
+ export function MerchantCustomPageBuilderView({ pageId, registry, className, children, }) {
36
+ const blocks = useThemePageBlocks(pageId, []);
37
+ if (blocks.length === 0) {
38
+ return null;
39
+ }
40
+ return (_jsxs("div", { className: className, children: [children, _jsx(BuilderPage, { pageId: pageId, blocks: blocks, registry: registry, context: null })] }));
41
+ }
42
+ export function useMerchantCustomPageRegistry(options) {
43
+ return useMemo(() => (options ? createMerchantCustomPageRegistry(options) : null), [options?.pageId, options?.CustomEmbed, options?.TextBlock]);
44
+ }
45
+ export function useMerchantCustomPageView(slug, components) {
46
+ const { settings } = useBuilderRuntime();
47
+ const customPage = slug ? findCustomPageBySlug(settings, slug) : undefined;
48
+ const registry = useMerchantCustomPageRegistry(customPage
49
+ ? {
50
+ pageId: customPage.id,
51
+ CustomEmbed: components.CustomEmbed,
52
+ TextBlock: components.TextBlock,
53
+ }
54
+ : null);
55
+ const builderBlockCount = customPage
56
+ ? (settings.theme.layout[customPage.id]?.blocks.length ?? 0)
57
+ : 0;
58
+ return {
59
+ customPage,
60
+ registry,
61
+ hasBuilderContent: builderBlockCount > 0,
62
+ };
63
+ }
@@ -0,0 +1,2 @@
1
+ export declare function isBuilderPreviewMode(location?: Pick<Location, 'search'>): boolean;
2
+ //# sourceMappingURL=preview-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-mode.d.ts","sourceRoot":"","sources":["../src/preview-mode.ts"],"names":[],"mappings":"AAAA,wBAAgB,oBAAoB,CAAC,QAAQ,GAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAmB,GAAG,OAAO,CAOlG"}
@@ -0,0 +1,7 @@
1
+ export function isBuilderPreviewMode(location = window.location) {
2
+ if (typeof window === 'undefined') {
3
+ return false;
4
+ }
5
+ const mode = new URLSearchParams(location.search).get('shoppex-preview-mode');
6
+ return mode === 'theme' || mode === 'builder';
7
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=react-runtime.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-runtime.test.d.ts","sourceRoot":"","sources":["../src/react-runtime.test.tsx"],"names":[],"mappings":""}