@shoppexio/builder-runtime 0.1.2 → 0.1.4

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 (57) 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.test.ts +22 -0
  51. package/src/preview-mode.ts +8 -0
  52. package/src/react.tsx +29 -4
  53. package/src/youtube-embed-block.test.ts +76 -0
  54. package/src/youtube-embed-block.ts +28 -0
  55. package/src/youtube-embed-builder-block.test.tsx +166 -0
  56. package/src/youtube.test.ts +48 -0
  57. package/src/youtube.ts +66 -0
@@ -0,0 +1,332 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
3
+ import { createEmptyBuilderSettings } from '@shoppex/builder-contracts';
4
+ import { JSDOM } from 'jsdom';
5
+ import { act } from 'react';
6
+ import { createRoot } from 'react-dom/client';
7
+ import { BuilderBlockFrame, BuilderBlockProvider, BuilderPage, BuilderRuntimePreviewProvider, useBuilderContent, useBuilderContentRecord, } from './react.js';
8
+ function createSettings(revision, title) {
9
+ return {
10
+ ...createEmptyBuilderSettings(revision),
11
+ theme: {
12
+ content: {
13
+ 'hero.title': title,
14
+ },
15
+ layout: {
16
+ home: {
17
+ blocks: [
18
+ {
19
+ id: 'hero-1',
20
+ type: 'hero',
21
+ visible: true,
22
+ settings: {},
23
+ },
24
+ ],
25
+ },
26
+ },
27
+ style_slots: {},
28
+ pages: [],
29
+ terms: {},
30
+ },
31
+ };
32
+ }
33
+ function Probe() {
34
+ const title = useBuilderContent('hero.title', '');
35
+ return (_jsx("section", { "data-builder-block": "hero-1", "data-builder-block-type": "hero", "data-page-id": "home", children: _jsx("h1", { "data-builder-content": "hero.title", children: title }) }));
36
+ }
37
+ function FallbackProbe() {
38
+ const title = useBuilderContent('hero.title', 'Default title');
39
+ return _jsx("h1", { "data-builder-content": "hero.title", children: title });
40
+ }
41
+ function SlotButtonProbe() {
42
+ return (_jsx("section", { "data-builder-block": "hero-1", "data-builder-block-type": "hero", "data-page-id": "home", children: _jsx("button", { type: "button", "data-builder-slot": "button.background", children: "Buy now" }) }));
43
+ }
44
+ function ScopedProbe() {
45
+ return (_jsx(BuilderBlockProvider, { block: {
46
+ id: 'hero-1',
47
+ type: 'hero',
48
+ visible: true,
49
+ settings: {
50
+ title: 'Scoped title',
51
+ subtitle: 'Scoped subtitle',
52
+ },
53
+ }, children: _jsx(ScopedProbeContent, {}) }));
54
+ }
55
+ function ScopedProbeContent() {
56
+ const title = useBuilderContent('hero.title', '');
57
+ const subtitle = useBuilderContent('hero.subtitle', '');
58
+ const contentRecord = useBuilderContentRecord();
59
+ return (_jsxs(_Fragment, { children: [_jsx("h1", { "data-testid": "scoped-title", children: title }), _jsx("p", { "data-testid": "scoped-subtitle", children: subtitle }), _jsx("span", { "data-testid": "scoped-record", children: String(contentRecord.hero.title) })] }));
60
+ }
61
+ function HeroBlock({ block }) {
62
+ const title = useBuilderContent('hero.title', '');
63
+ return (_jsx(BuilderBlockFrame, { as: "section", pageId: "home", block: block, children: _jsx("h1", { "data-builder-content": "hero.title", children: title }) }));
64
+ }
65
+ describe('BuilderRuntimePreviewProvider', () => {
66
+ let dom;
67
+ let root;
68
+ let postedMessages;
69
+ let parentWindow;
70
+ beforeEach(() => {
71
+ postedMessages = [];
72
+ parentWindow = {
73
+ postMessage: (message) => {
74
+ postedMessages.push(message);
75
+ },
76
+ };
77
+ dom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', {
78
+ url: 'https://preview.shoppex.test/?shoppex-preview-mode=theme',
79
+ referrer: 'https://dashboard.shoppex.test/theme/editor',
80
+ });
81
+ Object.defineProperty(dom.window, 'parent', {
82
+ configurable: true,
83
+ value: parentWindow,
84
+ });
85
+ Object.assign(globalThis, {
86
+ window: dom.window,
87
+ document: dom.window.document,
88
+ navigator: dom.window.navigator,
89
+ Element: dom.window.Element,
90
+ HTMLElement: dom.window.HTMLElement,
91
+ HTMLImageElement: dom.window.HTMLImageElement,
92
+ Node: dom.window.Node,
93
+ MouseEvent: dom.window.MouseEvent,
94
+ MessageEvent: dom.window.MessageEvent,
95
+ CustomEvent: dom.window.CustomEvent,
96
+ CSS: {
97
+ escape: (value) => value.replaceAll('"', '\\"'),
98
+ },
99
+ IS_REACT_ACT_ENVIRONMENT: true,
100
+ });
101
+ root = createRoot(dom.window.document.getElementById('root'));
102
+ });
103
+ afterEach(async () => {
104
+ await act(async () => {
105
+ root.unmount();
106
+ });
107
+ dom.window.close();
108
+ const globals = globalThis;
109
+ for (const key of [
110
+ 'window',
111
+ 'document',
112
+ 'navigator',
113
+ 'Element',
114
+ 'HTMLElement',
115
+ 'HTMLImageElement',
116
+ 'Node',
117
+ 'MouseEvent',
118
+ 'MessageEvent',
119
+ 'CustomEvent',
120
+ 'CSS',
121
+ 'IS_REACT_ACT_ENVIRONMENT',
122
+ ]) {
123
+ delete globals[key];
124
+ }
125
+ });
126
+ test('sends READY with the initial revision', async () => {
127
+ await act(async () => {
128
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(3, 'Initial title'), children: _jsx(Probe, {}) }));
129
+ });
130
+ expect(postedMessages).toContainEqual({
131
+ type: 'READY',
132
+ revision: 3,
133
+ health: {
134
+ reactMounted: true,
135
+ builderRuntimeProvider: true,
136
+ protocolVersion: 2,
137
+ },
138
+ });
139
+ expect(dom.window.document.body.textContent).toContain('Initial title');
140
+ });
141
+ test('normalizes mixed initial builder settings before strict validation', async () => {
142
+ await act(async () => {
143
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: {
144
+ version: 2,
145
+ revision: 9,
146
+ theme: {
147
+ content: {
148
+ 'hero.title': 'Mixed title',
149
+ },
150
+ layout: {
151
+ home: {
152
+ sections: [
153
+ {
154
+ id: 'hero',
155
+ visible: true,
156
+ },
157
+ ],
158
+ },
159
+ },
160
+ tokens_override: {
161
+ colors: {
162
+ primary: '#111827',
163
+ },
164
+ },
165
+ },
166
+ }, children: _jsx(Probe, {}) }));
167
+ });
168
+ expect(postedMessages).toContainEqual({
169
+ type: 'READY',
170
+ revision: 9,
171
+ health: {
172
+ reactMounted: true,
173
+ builderRuntimeProvider: true,
174
+ protocolVersion: 2,
175
+ },
176
+ });
177
+ expect(dom.window.document.body.textContent).toContain('Mixed title');
178
+ });
179
+ test('preserves intentionally empty content strings in hooks', async () => {
180
+ await act(async () => {
181
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(4, ''), children: _jsx(FallbackProbe, {}) }));
182
+ });
183
+ expect(dom.window.document.body.textContent).toBe('');
184
+ });
185
+ test('responds to explicit READY requests when the parent missed the initial message', async () => {
186
+ await act(async () => {
187
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(3, 'Initial title'), children: _jsx(Probe, {}) }));
188
+ });
189
+ postedMessages.length = 0;
190
+ await act(async () => {
191
+ dom.window.dispatchEvent(new dom.window.MessageEvent('message', {
192
+ origin: 'https://dashboard.shoppex.test',
193
+ source: parentWindow,
194
+ data: {
195
+ type: 'REQUEST_READY',
196
+ },
197
+ }));
198
+ });
199
+ expect(postedMessages).toEqual([{
200
+ type: 'READY',
201
+ revision: 3,
202
+ health: {
203
+ reactMounted: true,
204
+ builderRuntimeProvider: true,
205
+ protocolVersion: 2,
206
+ },
207
+ }]);
208
+ });
209
+ test('reports iframe runtime errors to the builder parent', async () => {
210
+ await act(async () => {
211
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(6, 'Initial title'), children: _jsx(Probe, {}) }));
212
+ });
213
+ postedMessages.length = 0;
214
+ dom.window.dispatchEvent(new dom.window.ErrorEvent('error', {
215
+ error: new Error('Preview crashed'),
216
+ message: 'Preview crashed',
217
+ }));
218
+ expect(postedMessages).toEqual([expect.objectContaining({
219
+ type: 'PREVIEW_ERROR',
220
+ revision: 6,
221
+ message: 'Preview crashed',
222
+ source: 'error',
223
+ })]);
224
+ });
225
+ test('applies preview state and acknowledges the exact revision', async () => {
226
+ await act(async () => {
227
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(3, 'Initial title'), children: _jsx(Probe, {}) }));
228
+ });
229
+ await act(async () => {
230
+ dom.window.dispatchEvent(new dom.window.MessageEvent('message', {
231
+ origin: 'https://dashboard.shoppex.test',
232
+ source: parentWindow,
233
+ data: {
234
+ type: 'APPLY_STATE',
235
+ revision: 4,
236
+ state: createSettings(4, 'Updated title'),
237
+ },
238
+ }));
239
+ });
240
+ expect(postedMessages).toContainEqual({ type: 'APPLIED', revision: 4 });
241
+ expect(dom.window.document.body.textContent).toContain('Updated title');
242
+ });
243
+ test('renders page blocks through a typed registry and shared block frame', async () => {
244
+ await act(async () => {
245
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(3, 'Registry title'), children: _jsx(BuilderPage, { pageId: "home", registry: {
246
+ hero: HeroBlock,
247
+ }, context: {} }) }));
248
+ });
249
+ const renderedBlock = dom.window.document.querySelector('[data-builder-block="hero-1"]');
250
+ expect(renderedBlock?.getAttribute('data-page-id')).toBe('home');
251
+ expect(renderedBlock?.getAttribute('data-builder-block-type')).toBe('hero');
252
+ expect(dom.window.document.body.textContent).toContain('Registry title');
253
+ });
254
+ test('prefers scoped block settings over global builder content inside a block provider', async () => {
255
+ await act(async () => {
256
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(3, 'Global title'), children: _jsx(ScopedProbe, {}) }));
257
+ });
258
+ expect(dom.window.document.querySelector('[data-testid="scoped-title"]')?.textContent).toBe('Scoped title');
259
+ expect(dom.window.document.querySelector('[data-testid="scoped-subtitle"]')?.textContent).toBe('Scoped subtitle');
260
+ expect(dom.window.document.querySelector('[data-testid="scoped-record"]')?.textContent).toBe('Scoped title');
261
+ });
262
+ test('rejects stale preview revisions without changing rendered content', async () => {
263
+ await act(async () => {
264
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(5, 'Current title'), children: _jsx(Probe, {}) }));
265
+ });
266
+ await act(async () => {
267
+ dom.window.dispatchEvent(new dom.window.MessageEvent('message', {
268
+ origin: 'https://dashboard.shoppex.test',
269
+ source: parentWindow,
270
+ data: {
271
+ type: 'APPLY_STATE',
272
+ revision: 4,
273
+ state: createSettings(4, 'Stale title'),
274
+ },
275
+ }));
276
+ });
277
+ expect(postedMessages).toContainEqual({
278
+ type: 'APPLY_FAILED',
279
+ revision: 4,
280
+ error: 'Stale builder revision',
281
+ });
282
+ expect(dom.window.document.body.textContent).toContain('Current title');
283
+ });
284
+ test('emits block selection details from iframe clicks', async () => {
285
+ await act(async () => {
286
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(7, 'Clickable title'), children: _jsx(Probe, {}) }));
287
+ });
288
+ const heading = dom.window.document.querySelector('h1');
289
+ heading?.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true, cancelable: true }));
290
+ expect(postedMessages).toContainEqual({
291
+ type: 'ELEMENT_CLICKED',
292
+ revision: 7,
293
+ selection: {
294
+ pageId: 'home',
295
+ blockId: 'hero-1',
296
+ blockType: 'hero',
297
+ contentPath: 'hero.title',
298
+ elementType: 'text',
299
+ },
300
+ });
301
+ });
302
+ test('classifies buttons before style slots for inspector clicks', async () => {
303
+ await act(async () => {
304
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(7, 'Clickable title'), children: _jsx(SlotButtonProbe, {}) }));
305
+ });
306
+ const button = dom.window.document.querySelector('button');
307
+ button?.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true, cancelable: true }));
308
+ expect(postedMessages).toContainEqual({
309
+ type: 'ELEMENT_CLICKED',
310
+ revision: 7,
311
+ selection: {
312
+ pageId: 'home',
313
+ blockId: 'hero-1',
314
+ blockType: 'hero',
315
+ slotId: 'button.background',
316
+ elementType: 'button',
317
+ },
318
+ });
319
+ });
320
+ test('does not intercept clicks outside trusted builder preview embeds', async () => {
321
+ dom.reconfigure({
322
+ url: 'https://preview.shoppex.test/',
323
+ });
324
+ await act(async () => {
325
+ root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(7, 'Clickable title'), children: _jsx(Probe, {}) }));
326
+ });
327
+ postedMessages.length = 0;
328
+ const heading = dom.window.document.querySelector('h1');
329
+ heading?.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true, cancelable: true }));
330
+ expect(postedMessages).toEqual([]);
331
+ });
332
+ });
package/dist/react.d.ts CHANGED
@@ -110,5 +110,11 @@ export declare function resolvePreviewReloadTarget(location: Pick<Location, 'sea
110
110
  export { getBuilderBlockSettingText, getBuilderProductBlockAttributes, getLayoutPageBlockAttributes, getProductBlockText, getProductPageBlockAttributes, } from './product-page.js';
111
111
  export { createStandardProductBlockRegistry, splitStandardProductPageBlocks, buildStandardProductInfoTabs, resolveStandardBuyBoxLabels, resolveStandardDetailsLabels, resolveStandardRelatedProductsTitle, resolveScopedBlockSettingText, STANDARD_PRODUCT_BLOCK_TYPES, STANDARD_PRODUCT_PRIMARY_BLOCK_TYPES, STANDARD_PRODUCT_SECONDARY_BLOCK_TYPES, } from './standard-product-blocks.js';
112
112
  export type { StandardBuyBoxLabels, StandardDetailsLabels, StandardProductBlockRegistryData, StandardProductBlockRegistryOptions, StandardProductBlockRegistrySlots, StandardProductBlockType, StandardProductSettingScope, StandardProductTabSpec, } from './standard-product-blocks.js';
113
+ export { YouTubeEmbed, YouTubeEmbedPreviewPlaceholder, type YouTubeEmbedProps } from './YouTubeEmbed.js';
114
+ export { YouTubeEmbedBuilderBlock, type YouTubeEmbedBuilderBlockProps, } from './YouTubeEmbedBuilderBlock.js';
115
+ export { createMerchantCustomPageRegistry, MerchantCustomPageBuilderView, useMerchantCustomPageRegistry, useMerchantCustomPageView, type MerchantCustomPageRegistryOptions, } from './merchant-custom-page.js';
116
+ export { isBuilderPreviewMode } from './preview-mode.js';
117
+ export { getYouTubeEmbedBlockStyleProps, readYouTubeEmbedBlockSettings, type YouTubeEmbedBlockInstance, } from './youtube-embed-block.js';
118
+ export { readManifestStyleBlockProps } from './block-style-settings.js';
113
119
  export { getBuilderPreviewReviewFixtures } from './preview-fixtures.js';
114
120
  //# sourceMappingURL=react.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"}
package/dist/react.js CHANGED
@@ -2,6 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { BuilderSettingsSchema, PreviewMessageSchema, createEmptyBuilderSettings, isTrustedPreviewParentOrigin, migrateLegacyBuilderSettings, normalizeDedicatedPageId, } from '@shoppex/builder-contracts';
3
3
  import { createElement, createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react';
4
4
  import { getBlockSettingValue, getBuilderContentList, getBuilderContentRecord, getBuilderContentString, getBuilderContentValue, } from './content.js';
5
+ import { resolveManifestSettingRecordValue } from './manifest-setting-paths.js';
5
6
  import { BUILDER_PREVIEW_REVIEWS } from './preview-fixtures.js';
6
7
  import { createBuilderCss } from './css-vars.js';
7
8
  import { builderBlock } from './attributes.js';
@@ -352,12 +353,16 @@ function useScopedBuilderContentValue(path) {
352
353
  const block = useContext(BuilderBlockContext);
353
354
  if (!block)
354
355
  return undefined;
355
- if (Object.prototype.hasOwnProperty.call(block.settings, path)) {
356
- return block.settings[path];
356
+ const resolved = resolveManifestSettingRecordValue(block.settings, path);
357
+ if (resolved !== undefined) {
358
+ return resolved;
357
359
  }
358
360
  const shortPath = path.startsWith(`${block.type}.`) ? path.slice(block.type.length + 1) : path.split('.').at(-1);
359
- if (shortPath && Object.prototype.hasOwnProperty.call(block.settings, shortPath)) {
360
- return block.settings[shortPath];
361
+ if (shortPath) {
362
+ const shortResolved = resolveManifestSettingRecordValue(block.settings, shortPath);
363
+ if (shortResolved !== undefined) {
364
+ return shortResolved;
365
+ }
361
366
  }
362
367
  const nested = shortPath ? getNestedBuilderSetting(block.settings, shortPath) : undefined;
363
368
  return nested ?? getNestedBuilderSetting(block.settings, path);
@@ -661,6 +666,7 @@ function installBuilderPreviewHoverInspector() {
661
666
  // we keep a small override table for these.
662
667
  const SLUG_LABEL_OVERRIDES = {
663
668
  'custom-html': 'Custom Embed',
669
+ 'youtube-embed': 'YouTube Video',
664
670
  };
665
671
  const override = SLUG_LABEL_OVERRIDES[type];
666
672
  const label = override ?? type
@@ -964,4 +970,10 @@ function installBuilderDirectManipulation(postMessage, getRevision) {
964
970
  }
965
971
  export { getBuilderBlockSettingText, getBuilderProductBlockAttributes, getLayoutPageBlockAttributes, getProductBlockText, getProductPageBlockAttributes, } from './product-page.js';
966
972
  export { createStandardProductBlockRegistry, splitStandardProductPageBlocks, buildStandardProductInfoTabs, resolveStandardBuyBoxLabels, resolveStandardDetailsLabels, resolveStandardRelatedProductsTitle, resolveScopedBlockSettingText, STANDARD_PRODUCT_BLOCK_TYPES, STANDARD_PRODUCT_PRIMARY_BLOCK_TYPES, STANDARD_PRODUCT_SECONDARY_BLOCK_TYPES, } from './standard-product-blocks.js';
973
+ export { YouTubeEmbed, YouTubeEmbedPreviewPlaceholder } from './YouTubeEmbed.js';
974
+ export { YouTubeEmbedBuilderBlock, } from './YouTubeEmbedBuilderBlock.js';
975
+ export { createMerchantCustomPageRegistry, MerchantCustomPageBuilderView, useMerchantCustomPageRegistry, useMerchantCustomPageView, } from './merchant-custom-page.js';
976
+ export { isBuilderPreviewMode } from './preview-mode.js';
977
+ export { getYouTubeEmbedBlockStyleProps, readYouTubeEmbedBlockSettings, } from './youtube-embed-block.js';
978
+ export { readManifestStyleBlockProps } from './block-style-settings.js';
967
979
  export { getBuilderPreviewReviewFixtures } from './preview-fixtures.js';
@@ -0,0 +1,10 @@
1
+ import type { BlockInstance } from '@shoppex/builder-contracts';
2
+ export type YouTubeEmbedBlockInstance = Pick<BlockInstance, 'id' | 'settings'>;
3
+ export declare function readYouTubeEmbedBlockSettings(block: YouTubeEmbedBlockInstance): {
4
+ videoUrl: string;
5
+ title: string;
6
+ height: number | undefined;
7
+ privacyEnhanced: boolean;
8
+ };
9
+ export declare function getYouTubeEmbedBlockStyleProps(block: YouTubeEmbedBlockInstance): import("react").CSSProperties;
10
+ //# sourceMappingURL=youtube-embed-block.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"youtube-embed-block.d.ts","sourceRoot":"","sources":["../src/youtube-embed-block.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGhE,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,UAAU,CAAC,CAAC;AAE/E,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,yBAAyB;;;;;EAkB7E;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,yBAAyB,iCAE9E"}
@@ -0,0 +1,19 @@
1
+ import { readManifestStyleBlockProps } from './block-style-settings.js';
2
+ export function readYouTubeEmbedBlockSettings(block) {
3
+ const videoUrl = typeof block.settings.videoUrl === 'string' ? block.settings.videoUrl : '';
4
+ const title = typeof block.settings.title === 'string' && block.settings.title.trim().length > 0
5
+ ? block.settings.title.trim()
6
+ : 'YouTube video';
7
+ const heightRaw = block.settings.height;
8
+ const height = typeof heightRaw === 'number' && heightRaw > 0 ? heightRaw : undefined;
9
+ const privacyEnhanced = block.settings.privacyEnhanced === true;
10
+ return {
11
+ videoUrl,
12
+ title,
13
+ height,
14
+ privacyEnhanced,
15
+ };
16
+ }
17
+ export function getYouTubeEmbedBlockStyleProps(block) {
18
+ return readManifestStyleBlockProps(block.settings);
19
+ }
@@ -0,0 +1,5 @@
1
+ export declare function parseYouTubeVideoId(input: string): string | null;
2
+ export declare function buildYouTubeEmbedSrc(videoId: string, options?: {
3
+ privacyEnhanced?: boolean;
4
+ }): string;
5
+ //# sourceMappingURL=youtube.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"youtube.d.ts","sourceRoot":"","sources":["../src/youtube.ts"],"names":[],"mappings":"AAMA,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA+ChE;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GACtC,MAAM,CAOR"}
@@ -0,0 +1,52 @@
1
+ const YOUTUBE_VIDEO_ID_PATTERN = /^[\w-]{11}$/;
2
+ function isYouTubeVideoId(value) {
3
+ return YOUTUBE_VIDEO_ID_PATTERN.test(value);
4
+ }
5
+ export function parseYouTubeVideoId(input) {
6
+ const trimmed = input.trim();
7
+ if (!trimmed) {
8
+ return null;
9
+ }
10
+ if (isYouTubeVideoId(trimmed)) {
11
+ return trimmed;
12
+ }
13
+ try {
14
+ const url = trimmed.startsWith('http://') || trimmed.startsWith('https://')
15
+ ? new URL(trimmed)
16
+ : new URL(`https://${trimmed}`);
17
+ const host = url.hostname.replace(/^www\./, '');
18
+ if (host === 'youtu.be') {
19
+ const candidate = url.pathname.split('/').filter(Boolean)[0] ?? '';
20
+ return isYouTubeVideoId(candidate) ? candidate : null;
21
+ }
22
+ if (host === 'youtube.com' || host === 'm.youtube.com' || host === 'music.youtube.com') {
23
+ const watchId = url.searchParams.get('v');
24
+ if (watchId && isYouTubeVideoId(watchId)) {
25
+ return watchId;
26
+ }
27
+ const embedMatch = url.pathname.match(/^\/embed\/([\w-]{11})/);
28
+ if (embedMatch?.[1] && isYouTubeVideoId(embedMatch[1])) {
29
+ return embedMatch[1];
30
+ }
31
+ const shortsMatch = url.pathname.match(/^\/shorts\/([\w-]{11})/);
32
+ if (shortsMatch?.[1] && isYouTubeVideoId(shortsMatch[1])) {
33
+ return shortsMatch[1];
34
+ }
35
+ const liveMatch = url.pathname.match(/^\/live\/([\w-]{11})/);
36
+ if (liveMatch?.[1] && isYouTubeVideoId(liveMatch[1])) {
37
+ return liveMatch[1];
38
+ }
39
+ }
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ return null;
45
+ }
46
+ export function buildYouTubeEmbedSrc(videoId, options) {
47
+ if (!isYouTubeVideoId(videoId)) {
48
+ throw new Error(`Invalid YouTube video id: ${videoId}`);
49
+ }
50
+ const host = options?.privacyEnhanced ? 'www.youtube-nocookie.com' : 'www.youtube.com';
51
+ return `https://${host}/embed/${videoId}`;
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shoppexio/builder-runtime",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Theme-side Builder v2 runtime helpers for Shoppex storefront themes",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { buildYouTubeEmbedSrc, parseYouTubeVideoId } from './youtube.js';
5
+
6
+ export interface YouTubeEmbedProps {
7
+ videoUrl?: string;
8
+ title?: string;
9
+ height?: number;
10
+ privacyEnhanced?: boolean;
11
+ className?: string;
12
+ }
13
+
14
+ const DEFAULT_HEIGHT = 400;
15
+ const EMBED_MAX_WIDTH = 640;
16
+
17
+ export function YouTubeEmbed({
18
+ videoUrl = '',
19
+ title = 'YouTube video',
20
+ height,
21
+ privacyEnhanced = false,
22
+ className = '',
23
+ }: YouTubeEmbedProps) {
24
+ const videoId = useMemo(() => parseYouTubeVideoId(videoUrl), [videoUrl]);
25
+ const resolvedHeight =
26
+ typeof height === 'number' && height > 0 ? height : DEFAULT_HEIGHT;
27
+
28
+ if (!videoId) {
29
+ return null;
30
+ }
31
+
32
+ return (
33
+ <section className={`my-8 ${className}`} style={{ display: 'flex', justifyContent: 'center' }}>
34
+ <div
35
+ style={{
36
+ position: 'relative',
37
+ width: '100%',
38
+ maxWidth: `${EMBED_MAX_WIDTH}px`,
39
+ height: `${resolvedHeight}px`,
40
+ borderRadius: 'inherit',
41
+ }}
42
+ >
43
+ <iframe
44
+ src={buildYouTubeEmbedSrc(videoId, { privacyEnhanced })}
45
+ title={title}
46
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
47
+ allowFullScreen
48
+ loading="lazy"
49
+ style={{
50
+ display: 'block',
51
+ width: '100%',
52
+ height: '100%',
53
+ border: 0,
54
+ }}
55
+ />
56
+ </div>
57
+ </section>
58
+ );
59
+ }
60
+
61
+ export function YouTubeEmbedPreviewPlaceholder({
62
+ className = '',
63
+ height,
64
+ }: {
65
+ className?: string;
66
+ height?: number;
67
+ }) {
68
+ const resolvedHeight =
69
+ typeof height === 'number' && height > 0 ? height : DEFAULT_HEIGHT;
70
+
71
+ return (
72
+ <section className={`my-8 ${className}`} style={{ display: 'flex', justifyContent: 'center' }}>
73
+ <div
74
+ style={{
75
+ position: 'relative',
76
+ width: '100%',
77
+ maxWidth: `${EMBED_MAX_WIDTH}px`,
78
+ height: `${resolvedHeight}px`,
79
+ borderRadius: 'inherit',
80
+ }}
81
+ >
82
+ <div
83
+ style={{
84
+ display: 'flex',
85
+ alignItems: 'center',
86
+ justifyContent: 'center',
87
+ width: '100%',
88
+ height: '100%',
89
+ padding: '0 24px',
90
+ border: '1px dashed rgba(127, 127, 127, 0.35)',
91
+ borderRadius: 'inherit',
92
+ background: 'rgba(127, 127, 127, 0.06)',
93
+ color: '#5f6470',
94
+ fontFamily: 'Inter, system-ui, sans-serif',
95
+ fontSize: '13px',
96
+ textAlign: 'center',
97
+ }}
98
+ aria-hidden="true"
99
+ >
100
+ Paste a YouTube link or video ID in the Inspector to preview the player here.
101
+ </div>
102
+ </div>
103
+ </section>
104
+ );
105
+ }