@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.
Files changed (90) 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 +8 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +8 -0
  19. package/dist/layout.d.ts +3 -8
  20. package/dist/layout.d.ts.map +1 -1
  21. package/dist/layout.js +2 -10
  22. package/dist/manifest-setting-paths.d.ts +5 -0
  23. package/dist/manifest-setting-paths.d.ts.map +1 -0
  24. package/dist/manifest-setting-paths.js +40 -0
  25. package/dist/merchant-custom-page.d.ts +57 -0
  26. package/dist/merchant-custom-page.d.ts.map +1 -0
  27. package/dist/merchant-custom-page.js +63 -0
  28. package/dist/preview-fixtures.d.ts +16 -0
  29. package/dist/preview-fixtures.d.ts.map +1 -0
  30. package/dist/preview-fixtures.js +40 -0
  31. package/dist/preview-mode.d.ts +2 -0
  32. package/dist/preview-mode.d.ts.map +1 -0
  33. package/dist/preview-mode.js +7 -0
  34. package/dist/product-page.d.ts +13 -0
  35. package/dist/product-page.d.ts.map +1 -0
  36. package/dist/product-page.js +18 -0
  37. package/dist/react-runtime.test.d.ts +2 -0
  38. package/dist/react-runtime.test.d.ts.map +1 -0
  39. package/dist/react-runtime.test.js +332 -0
  40. package/dist/react.d.ts +37 -2
  41. package/dist/react.d.ts.map +1 -1
  42. package/dist/react.js +138 -46
  43. package/dist/search-bar-settings.d.ts +33 -0
  44. package/dist/search-bar-settings.d.ts.map +1 -0
  45. package/dist/search-bar-settings.js +99 -0
  46. package/dist/standard-product-blocks.d.ts +48 -0
  47. package/dist/standard-product-blocks.d.ts.map +1 -0
  48. package/dist/standard-product-blocks.js +45 -0
  49. package/dist/standard-product-page.d.ts +69 -0
  50. package/dist/standard-product-page.d.ts.map +1 -0
  51. package/dist/standard-product-page.js +89 -0
  52. package/dist/storefront-google-fonts.d.ts +2 -0
  53. package/dist/storefront-google-fonts.d.ts.map +1 -0
  54. package/dist/storefront-google-fonts.js +28 -0
  55. package/dist/youtube-embed-block.d.ts +10 -0
  56. package/dist/youtube-embed-block.d.ts.map +1 -0
  57. package/dist/youtube-embed-block.js +19 -0
  58. package/dist/youtube.d.ts +5 -0
  59. package/dist/youtube.d.ts.map +1 -0
  60. package/dist/youtube.js +52 -0
  61. package/package.json +3 -3
  62. package/src/YouTubeEmbed.tsx +105 -0
  63. package/src/YouTubeEmbedBuilderBlock.tsx +49 -0
  64. package/src/block-style-settings.ts +24 -0
  65. package/src/builder-runtime.test.ts +69 -0
  66. package/src/content.ts +44 -9
  67. package/src/index.ts +8 -0
  68. package/src/layout.ts +11 -21
  69. package/src/manifest-setting-paths.test.ts +23 -0
  70. package/src/manifest-setting-paths.ts +55 -0
  71. package/src/merchant-custom-page.tsx +161 -0
  72. package/src/preview-fixtures.ts +56 -0
  73. package/src/preview-mode.ts +8 -0
  74. package/src/product-page.test.ts +37 -0
  75. package/src/product-page.ts +32 -0
  76. package/src/react-runtime.test.tsx +42 -0
  77. package/src/react.tsx +243 -49
  78. package/src/search-bar-settings.test.ts +72 -0
  79. package/src/search-bar-settings.ts +176 -0
  80. package/src/standard-product-blocks.test.tsx +93 -0
  81. package/src/standard-product-blocks.tsx +121 -0
  82. package/src/standard-product-page.test.ts +171 -0
  83. package/src/standard-product-page.ts +169 -0
  84. package/src/storefront-google-fonts.test.ts +31 -0
  85. package/src/storefront-google-fonts.ts +43 -0
  86. package/src/youtube-embed-block.test.ts +76 -0
  87. package/src/youtube-embed-block.ts +28 -0
  88. package/src/youtube-embed-builder-block.test.tsx +166 -0
  89. package/src/youtube.test.ts +48 -0
  90. package/src/youtube.ts +66 -0
package/dist/react.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { BuilderSettingsSchema, PreviewMessageSchema, createEmptyBuilderSettings, migrateLegacyBuilderSettings, } from '@shoppex/builder-contracts';
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';
6
+ import { BUILDER_PREVIEW_REVIEWS } from './preview-fixtures.js';
5
7
  import { createBuilderCss } from './css-vars.js';
8
+ import { builderBlock } from './attributes.js';
6
9
  import { getPageBlocks, getPageLayout, getVisiblePageBlocks } from './layout.js';
10
+ import { getNavigationHeaderSettings, resolveSearchBarSettings, } from './search-bar-settings.js';
7
11
  import { resolveStyleSlotValue } from './style-slots.js';
8
12
  const BUILDER_SETTINGS_WINDOW_KEY = '__SHOPPEX_BUILDER_SETTINGS__';
9
13
  const BUILDER_STATE_EVENT_NAME = 'shoppex:builder-state';
@@ -155,6 +159,7 @@ export function BuilderRuntimePreviewProvider({ initialSettings, children, }) {
155
159
  message: normalized.message,
156
160
  ...(normalized.stack ? { stack: normalized.stack } : {}),
157
161
  source,
162
+ phase: 'runtime',
158
163
  diagnostics: {
159
164
  name: normalized.name,
160
165
  href: window.location.href,
@@ -295,13 +300,48 @@ export function useVisibleBuilderPageBlocks(pageId) {
295
300
  }
296
301
  export function useThemePageBlocks(pageId, defaultOrder) {
297
302
  const { settings } = useBuilderRuntime();
303
+ const canonicalPageId = normalizeDedicatedPageId(pageId);
298
304
  return useMemo(() => {
299
- const page = settings.theme.layout[pageId];
305
+ const page = settings.theme.layout[canonicalPageId];
300
306
  if (page) {
301
307
  return page.blocks.filter((block) => block.visible);
302
308
  }
303
309
  return defaultOrder.map((type) => ({ id: type, type, visible: true, settings: {} }));
304
- }, [defaultOrder, pageId, settings.theme.layout]);
310
+ }, [canonicalPageId, defaultOrder, settings.theme.layout]);
311
+ }
312
+ export function useThemePageBlockAttributes(pageId, defaultOrder) {
313
+ const canonicalPageId = normalizeDedicatedPageId(pageId);
314
+ const blocks = useThemePageBlocks(pageId, defaultOrder);
315
+ const block = blocks[0];
316
+ return useMemo(() => ({
317
+ 'data-page-id': canonicalPageId,
318
+ ...(block ? builderBlock(block.id, block.type) : {}),
319
+ }), [block, canonicalPageId]);
320
+ }
321
+ export function DedicatedBuilderPage({ pageId, defaultBlockOrder, as, children, ...rest }) {
322
+ const attrs = useThemePageBlockAttributes(pageId, defaultBlockOrder);
323
+ const Component = (as ?? 'section');
324
+ return createElement(Component, { ...attrs, ...rest }, children);
325
+ }
326
+ export function useBuilderPreviewReviews(input) {
327
+ const fixtures = input.fixtures ?? BUILDER_PREVIEW_REVIEWS;
328
+ const shouldUseFixtures = isBuilderPreviewRuntime() && Boolean(input.error);
329
+ return useMemo(() => {
330
+ if (!shouldUseFixtures) {
331
+ return {
332
+ reviews: input.reviews,
333
+ isLoading: input.isLoading,
334
+ error: input.error,
335
+ isUsingFixtures: false,
336
+ };
337
+ }
338
+ return {
339
+ reviews: fixtures,
340
+ isLoading: false,
341
+ error: null,
342
+ isUsingFixtures: true,
343
+ };
344
+ }, [fixtures, input.error, input.isLoading, input.reviews, shouldUseFixtures]);
305
345
  }
306
346
  export function useBuilderStyleSlot(slotId, input = {}) {
307
347
  return resolveStyleSlotValue(useBuilderRuntime().settings, slotId, input);
@@ -313,12 +353,16 @@ function useScopedBuilderContentValue(path) {
313
353
  const block = useContext(BuilderBlockContext);
314
354
  if (!block)
315
355
  return undefined;
316
- if (Object.prototype.hasOwnProperty.call(block.settings, path)) {
317
- return block.settings[path];
356
+ const resolved = resolveManifestSettingRecordValue(block.settings, path);
357
+ if (resolved !== undefined) {
358
+ return resolved;
318
359
  }
319
360
  const shortPath = path.startsWith(`${block.type}.`) ? path.slice(block.type.length + 1) : path.split('.').at(-1);
320
- if (shortPath && Object.prototype.hasOwnProperty.call(block.settings, shortPath)) {
321
- return block.settings[shortPath];
361
+ if (shortPath) {
362
+ const shortResolved = resolveManifestSettingRecordValue(block.settings, shortPath);
363
+ if (shortResolved !== undefined) {
364
+ return shortResolved;
365
+ }
322
366
  }
323
367
  const nested = shortPath ? getNestedBuilderSetting(block.settings, shortPath) : undefined;
324
368
  return nested ?? getNestedBuilderSetting(block.settings, path);
@@ -333,7 +377,42 @@ function getNestedBuilderSetting(record, path) {
333
377
  }
334
378
  return current;
335
379
  }
336
- function isBuilderPreviewRuntime() {
380
+ export function useSearchBarSettings(input) {
381
+ const runtime = useBuilderRuntime();
382
+ const headerSettings = useMemo(() => input.headerSettings ?? getNavigationHeaderSettings(runtime.settings), [input.headerSettings, runtime.settings]);
383
+ const heroPlaceholder = useBuilderContentValue('hero.search.placeholder');
384
+ const heroBackground = useBuilderContentValue('hero.search.background');
385
+ const heroBorderColor = useBuilderContentValue('hero.search.borderColor');
386
+ const heroBorderRadius = useBuilderContentValue('hero.search.borderRadius');
387
+ const heroMaxWidth = useBuilderContentValue('hero.search.maxWidth');
388
+ const heroShowShortcut = useBuilderContentValue('hero.search.showShortcut');
389
+ const heroShow = useBuilderContentValue('hero.search.show');
390
+ return useMemo(() => resolveSearchBarSettings({
391
+ variant: input.variant,
392
+ headerSettings,
393
+ heroValues: {
394
+ 'hero.search.placeholder': heroPlaceholder,
395
+ 'hero.search.background': heroBackground,
396
+ 'hero.search.borderColor': heroBorderColor,
397
+ 'hero.search.borderRadius': heroBorderRadius,
398
+ 'hero.search.maxWidth': heroMaxWidth,
399
+ 'hero.search.showShortcut': heroShowShortcut,
400
+ 'hero.search.show': heroShow,
401
+ },
402
+ }), [
403
+ headerSettings,
404
+ heroBackground,
405
+ heroBorderColor,
406
+ heroBorderRadius,
407
+ heroMaxWidth,
408
+ heroPlaceholder,
409
+ heroShow,
410
+ heroShowShortcut,
411
+ input.variant,
412
+ ]);
413
+ }
414
+ export { buildSearchShellStyle, getNavigationHeaderSettings, resolveSearchBarSettings, } from './search-bar-settings.js';
415
+ export function isBuilderPreviewRuntime() {
337
416
  if (typeof window === 'undefined') {
338
417
  return false;
339
418
  }
@@ -423,7 +502,7 @@ function isTrustedBuilderPreviewEmbed(location, parentOrigin) {
423
502
  return false;
424
503
  if (!hasBuilderPreviewMode(location))
425
504
  return false;
426
- return isTrustedBuilderPreviewParentOrigin(parentOrigin);
505
+ return isTrustedPreviewParentOrigin(parentOrigin);
427
506
  }
428
507
  function hasBuilderPreviewMode(location) {
429
508
  const searchParams = new URLSearchParams(location.search);
@@ -431,23 +510,6 @@ function hasBuilderPreviewMode(location) {
431
510
  || searchParams.get('shoppex-preview-mode') === 'builder'
432
511
  || searchParams.get('shoppex-preview') === 'builder');
433
512
  }
434
- function isTrustedBuilderPreviewParentOrigin(origin) {
435
- try {
436
- const parsed = new URL(origin);
437
- const hostname = parsed.hostname.toLowerCase();
438
- return (hostname === 'dashboard.shoppex.io'
439
- || hostname === 'dashboard.shoppex.test'
440
- || hostname === 'localhost'
441
- || hostname === '127.0.0.1'
442
- || hostname === '::1'
443
- || hostname.endsWith('.localhost')
444
- || hostname.endsWith('.vercel.app')
445
- || hostname.endsWith('.vercel.run'));
446
- }
447
- catch {
448
- return false;
449
- }
450
- }
451
513
  function selectBuilderElement(blockId) {
452
514
  document
453
515
  .querySelectorAll(BUILDER_SELECTED_SELECTOR)
@@ -458,7 +520,9 @@ function selectBuilderElement(blockId) {
458
520
  if (!target)
459
521
  return;
460
522
  target.setAttribute('data-builder-selected', 'true');
461
- target.scrollIntoView({ behavior: 'smooth', block: 'center' });
523
+ const pageId = target.getAttribute('data-page-id');
524
+ const scrollBlock = pageId === 'footer' ? 'end' : pageId === 'navigation' ? 'start' : 'nearest';
525
+ target.scrollIntoView({ behavior: 'smooth', block: scrollBlock });
462
526
  }
463
527
  function createBuilderSelection(blockElement, target) {
464
528
  const contentElement = target.closest(BUILDER_CONTENT_SELECTOR);
@@ -602,6 +666,7 @@ function installBuilderPreviewHoverInspector() {
602
666
  // we keep a small override table for these.
603
667
  const SLUG_LABEL_OVERRIDES = {
604
668
  'custom-html': 'Custom Embed',
669
+ 'youtube-embed': 'YouTube Video',
605
670
  };
606
671
  const override = SLUG_LABEL_OVERRIDES[type];
607
672
  const label = override ?? type
@@ -641,7 +706,8 @@ function installBuilderPreviewHoverInspector() {
641
706
  window.removeEventListener('mouseout', handleMouseOut, true);
642
707
  };
643
708
  }
644
- const INSERTER_HOVER_BAND_PX = 16;
709
+ const INSERTER_HOVER_BAND_PX = 28;
710
+ const INSERTER_CLEAR_DELAY_MS = 150;
645
711
  /**
646
712
  * Direct-manipulation bridge: tracks the bounding rect of the selected
647
713
  * block (for the floating toolbar), the inter-block gap currently
@@ -731,26 +797,43 @@ function installBuilderDirectManipulation(postMessage, getRevision) {
731
797
  // a tracked block stack. We don't try to be clever about which gap;
732
798
  // every direct ancestor with a list of `[data-builder-block]` children
733
799
  // contributes one gap per pair.
800
+ let inserterClearTimer = null;
734
801
  const postInserter = (index, rect) => {
735
- const key = index === null || !rect
736
- ? '__none__'
737
- : `${index}|${rect.top}|${rect.left}|${rect.width}|${rect.height}`;
738
- if (key === lastInserterKey)
802
+ const emitInserter = () => {
803
+ const key = index === null || !rect
804
+ ? '__none__'
805
+ : `${index}|${rect.top}|${rect.left}|${rect.width}|${rect.height}`;
806
+ if (key === lastInserterKey)
807
+ return;
808
+ lastInserterKey = key;
809
+ postMessage({
810
+ type: 'INSERTER_HOVER',
811
+ revision: getRevision(),
812
+ index,
813
+ rect: rect === null
814
+ ? null
815
+ : {
816
+ top: rect.top,
817
+ left: rect.left,
818
+ width: rect.width,
819
+ height: rect.height,
820
+ },
821
+ });
822
+ };
823
+ if (index !== null && rect !== null) {
824
+ if (inserterClearTimer !== null) {
825
+ window.clearTimeout(inserterClearTimer);
826
+ inserterClearTimer = null;
827
+ }
828
+ emitInserter();
739
829
  return;
740
- lastInserterKey = key;
741
- postMessage({
742
- type: 'INSERTER_HOVER',
743
- revision: getRevision(),
744
- index,
745
- rect: rect === null
746
- ? null
747
- : {
748
- top: rect.top,
749
- left: rect.left,
750
- width: rect.width,
751
- height: rect.height,
752
- },
753
- });
830
+ }
831
+ if (inserterClearTimer !== null)
832
+ return;
833
+ inserterClearTimer = window.setTimeout(() => {
834
+ inserterClearTimer = null;
835
+ emitInserter();
836
+ }, INSERTER_CLEAR_DELAY_MS);
754
837
  };
755
838
  const handleMouseMove = (event) => {
756
839
  const blocks = Array.from(document.querySelectorAll(BUILDER_BLOCK_SELECTOR)).filter((el) => {
@@ -885,3 +968,12 @@ function installBuilderDirectManipulation(postMessage, getRevision) {
885
968
  window.removeEventListener('mousedown', handleMouseDown, true);
886
969
  };
887
970
  }
971
+ export { getBuilderBlockSettingText, getBuilderProductBlockAttributes, getLayoutPageBlockAttributes, getProductBlockText, getProductPageBlockAttributes, } from './product-page.js';
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';
979
+ export { getBuilderPreviewReviewFixtures } from './preview-fixtures.js';
@@ -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"}