@standhigher/puck-page-builder 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,7 +65,7 @@ const notice = {
65
65
  />
66
66
  ```
67
67
 
68
- Built-in field controls are `text` (single line), `textarea` (multi-line), `url`, and `color`. Their validation runs in the editor before save or publish; custom fields may continue using a block's `validate` function. `validateFieldValue` is exported from `/extensions` when a host needs the same validation outside the editor.
68
+ Built-in field controls are `text` (single line), `textarea` (multi-line), `url`, `color`, and `products`. `products` only renders the selected snapshot and a host picker button; Shopify search stays in the host `productPicker` adapter. Their validation runs in the editor before save or publish; custom fields may continue using a block's `validate` function. `validateFieldValue` is exported from `/extensions` when a host needs the same validation outside the editor.
69
69
 
70
70
  Theme values remain deliberately token-based: template theme → page theme → variant theme → block style. A block resets an inherited token by setting its local token explicitly (for example `{ radius: "0" }`), so no incompatible persisted style-reset shape is needed.
71
71
 
@@ -1,3 +1,46 @@
1
+ // src/core/extensions/product-reference.ts
2
+ function isHttpUrl(value) {
3
+ try {
4
+ const url = new URL(value);
5
+ return (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password;
6
+ } catch {
7
+ return false;
8
+ }
9
+ }
10
+ function isProductReference(value) {
11
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
12
+ const candidate = value;
13
+ if (typeof candidate.id !== "string" || !candidate.id.trim()) return false;
14
+ if (candidate.title !== void 0 && typeof candidate.title !== "string") return false;
15
+ if (candidate.handle !== void 0 && typeof candidate.handle !== "string") return false;
16
+ if (candidate.imageUrl !== void 0 && (typeof candidate.imageUrl !== "string" || !isHttpUrl(candidate.imageUrl))) return false;
17
+ return true;
18
+ }
19
+ function parseProductReferences(value) {
20
+ if (!Array.isArray(value)) return [];
21
+ const products = [];
22
+ for (const item of value) {
23
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
24
+ const candidate = item;
25
+ if (typeof candidate.id !== "string" || !candidate.id.trim()) continue;
26
+ products.push({
27
+ id: candidate.id.trim(),
28
+ ...typeof candidate.title === "string" && candidate.title.trim() ? { title: candidate.title.trim() } : {},
29
+ ...typeof candidate.handle === "string" && candidate.handle.trim() ? { handle: candidate.handle.trim() } : {},
30
+ ...typeof candidate.imageUrl === "string" && isHttpUrl(candidate.imageUrl) ? { imageUrl: candidate.imageUrl } : {}
31
+ });
32
+ }
33
+ return products;
34
+ }
35
+ function toProductReferenceJson(product) {
36
+ return {
37
+ id: product.id,
38
+ ...product.title ? { title: product.title } : {},
39
+ ...product.handle ? { handle: product.handle } : {},
40
+ ...product.imageUrl ? { imageUrl: product.imageUrl } : {}
41
+ };
42
+ }
43
+
1
44
  // src/core/extensions/validation.ts
2
45
  var hexColor = /^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i;
3
46
  function stringLength(value) {
@@ -19,6 +62,10 @@ function validateFieldValue(config, value) {
19
62
  const control = config.control;
20
63
  if (!control && !config.required && !config.validation) return [];
21
64
  if (value === void 0 && !config.required) return [];
65
+ if (control === "products") {
66
+ if (!Array.isArray(value)) return [{ path: "", message: "\u5FC5\u987B\u662F\u5546\u54C1\u5217\u8868\u3002" }];
67
+ return value.flatMap((item, index) => isProductReference(item) ? [] : [{ path: String(index), message: "\u5FC5\u987B\u5305\u542B\u7A33\u5B9A\u7684\u5546\u54C1 ID\u3002" }]);
68
+ }
22
69
  if (typeof value !== "string") return [{ path: "", message: "\u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002" }];
23
70
  const issues = [];
24
71
  if (config.required && value.trim().length === 0) issues.push({ path: "", message: "\u6B64\u5B57\u6BB5\u4E3A\u5FC5\u586B\u9879\u3002" });
@@ -314,6 +361,9 @@ function validatePageDocumentWithRegistry(document, registry) {
314
361
  }
315
362
 
316
363
  export {
364
+ isProductReference,
365
+ parseProductReferences,
366
+ toProductReferenceJson,
317
367
  validateFieldValue,
318
368
  validateBlockPolicy,
319
369
  mergeBlockPolicies,
@@ -1,6 +1,6 @@
1
- import { h as ExtensionRegistry, V as ValidationIssue, c as BlockPolicy, F as FieldConfig } from './registry-eNtkEC6o.js';
2
- export { B as BlockDefinition, a as BlockEditorProps, b as BlockOperation, d as BlockVariantDefinition, D as DataSourceDefinition, E as EditorAction, e as EditorActionPosition, f as EditorOperationPolicy, g as ExtensionActionContext, i as ExtensionRegistryError, j as ExtensionRegistryErrorCode, k as ExtensionRegistryOptions, l as ExtensionTarget, m as FieldDefinition, n as FieldProps, o as FieldValidation, L as LifecycleHooks, P as PageBuilderExtension, R as RendererDefinition, T as TemplateDefinition, p as TemplateRegistry, q as TemplateSource, U as UISlotContribution, r as UISlotName, s as createExtensionRegistry, t as createTemplateRegistry } from './registry-eNtkEC6o.js';
3
- import { PageDocument } from './schema.js';
1
+ import { h as ExtensionRegistry, V as ValidationIssue, c as BlockPolicy, F as FieldConfig } from './registry-CXJdOya3.js';
2
+ export { B as BlockDefinition, a as BlockEditorProps, b as BlockOperation, d as BlockVariantDefinition, D as DataSourceDefinition, E as EditorAction, e as EditorActionPosition, f as EditorOperationPolicy, g as ExtensionActionContext, i as ExtensionRegistryError, j as ExtensionRegistryErrorCode, k as ExtensionRegistryOptions, l as ExtensionTarget, m as FieldDefinition, n as FieldProps, o as FieldValidation, L as LifecycleHooks, P as PageBuilderExtension, R as RendererDefinition, T as TemplateDefinition, p as TemplateRegistry, q as TemplateSource, U as UISlotContribution, r as UISlotName, s as createExtensionRegistry, t as createTemplateRegistry } from './registry-CXJdOya3.js';
3
+ import { PageDocument, JsonValue } from './schema.js';
4
4
  import 'react';
5
5
  import './theme.js';
6
6
 
@@ -12,6 +12,20 @@ import './theme.js';
12
12
  */
13
13
  declare function validatePageDocumentWithRegistry(document: PageDocument, registry: ExtensionRegistry): ValidationIssue[];
14
14
 
15
+ /**
16
+ * JSON-only product snapshot persisted in PageDocument props.
17
+ * Hosts may add a stable id plus display fields; never store tokens or prices as secrets.
18
+ */
19
+ type ProductReference = {
20
+ id: string;
21
+ title?: string;
22
+ imageUrl?: string;
23
+ handle?: string;
24
+ };
25
+ declare function isProductReference(value: unknown): value is ProductReference;
26
+ declare function parseProductReferences(value: unknown): ProductReference[];
27
+ declare function toProductReferenceJson(product: ProductReference): JsonValue;
28
+
15
29
  /** Validates the portable built-in controls without relying on browser-only APIs. */
16
30
  declare function validateFieldValue(config: FieldConfig, value: unknown): ValidationIssue[];
17
31
  /** Returns a diagnostic for an invalid declaration, or `null` when it is usable. */
@@ -20,4 +34,4 @@ declare function mergeBlockPolicies(...policies: Array<BlockPolicy | undefined>)
20
34
  declare function minimumBlockInstances(policy: BlockPolicy | undefined): number;
21
35
  declare function maximumBlockInstances(policy: BlockPolicy | undefined): number;
22
36
 
23
- export { BlockPolicy, ExtensionRegistry, FieldConfig, ValidationIssue, maximumBlockInstances, mergeBlockPolicies, minimumBlockInstances, validateBlockPolicy, validateFieldValue, validatePageDocumentWithRegistry };
37
+ export { BlockPolicy, ExtensionRegistry, FieldConfig, type ProductReference, ValidationIssue, isProductReference, maximumBlockInstances, mergeBlockPolicies, minimumBlockInstances, parseProductReferences, toProductReferenceJson, validateBlockPolicy, validateFieldValue, validatePageDocumentWithRegistry };
@@ -4,22 +4,28 @@ import {
4
4
  TemplateRegistry,
5
5
  createExtensionRegistry,
6
6
  createTemplateRegistry,
7
+ isProductReference,
7
8
  maximumBlockInstances,
8
9
  mergeBlockPolicies,
9
10
  minimumBlockInstances,
11
+ parseProductReferences,
12
+ toProductReferenceJson,
10
13
  validateBlockPolicy,
11
14
  validateFieldValue,
12
15
  validatePageDocumentWithRegistry
13
- } from "./chunk-PFCWXLWN.js";
16
+ } from "./chunk-YXBTNYPR.js";
14
17
  export {
15
18
  ExtensionRegistry,
16
19
  ExtensionRegistryError,
17
20
  TemplateRegistry,
18
21
  createExtensionRegistry,
19
22
  createTemplateRegistry,
23
+ isProductReference,
20
24
  maximumBlockInstances,
21
25
  mergeBlockPolicies,
22
26
  minimumBlockInstances,
27
+ parseProductReferences,
28
+ toProductReferenceJson,
23
29
  validateBlockPolicy,
24
30
  validateFieldValue,
25
31
  validatePageDocumentWithRegistry
package/dist/index.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { f as EditorOperationPolicy, c as BlockPolicy, V as ValidationIssue, h as ExtensionRegistry } from './registry-eNtkEC6o.js';
4
- export { B as BlockDefinition, a as BlockEditorProps, b as BlockOperation, d as BlockVariantDefinition, D as DataSourceDefinition, E as EditorAction, e as EditorActionPosition, g as ExtensionActionContext, i as ExtensionRegistryError, j as ExtensionRegistryErrorCode, k as ExtensionRegistryOptions, l as ExtensionTarget, F as FieldConfig, m as FieldDefinition, n as FieldProps, o as FieldValidation, L as LifecycleHooks, P as PageBuilderExtension, R as RendererDefinition, T as TemplateDefinition, p as TemplateRegistry, q as TemplateSource, U as UISlotContribution, r as UISlotName, s as createExtensionRegistry, t as createTemplateRegistry } from './registry-eNtkEC6o.js';
3
+ import { f as EditorOperationPolicy, c as BlockPolicy, V as ValidationIssue, h as ExtensionRegistry } from './registry-CXJdOya3.js';
4
+ export { B as BlockDefinition, a as BlockEditorProps, b as BlockOperation, d as BlockVariantDefinition, D as DataSourceDefinition, E as EditorAction, e as EditorActionPosition, g as ExtensionActionContext, i as ExtensionRegistryError, j as ExtensionRegistryErrorCode, k as ExtensionRegistryOptions, l as ExtensionTarget, F as FieldConfig, m as FieldDefinition, n as FieldProps, o as FieldValidation, L as LifecycleHooks, P as PageBuilderExtension, R as RendererDefinition, T as TemplateDefinition, p as TemplateRegistry, q as TemplateSource, U as UISlotContribution, r as UISlotName, s as createExtensionRegistry, t as createTemplateRegistry } from './registry-CXJdOya3.js';
5
5
  import { PageDocument, BlockNode, JsonValue } from './schema.js';
6
6
  export { BlockStyleOverrides, DataBinding, PageDocumentIssue, PageDocumentMigration, PageDocumentSchemaVersion, PageSettings, RenderTarget, createPageDocument, migratePageDocument, validatePageDocument } from './schema.js';
7
+ import { ProductReference } from './extensions.js';
8
+ export { isProductReference, maximumBlockInstances, mergeBlockPolicies, minimumBlockInstances, parseProductReferences, toProductReferenceJson, validateBlockPolicy, validateFieldValue, validatePageDocumentWithRegistry } from './extensions.js';
7
9
  export { ThemeTokenName, ThemeTokens, mergeThemeTokens, normalizeThemeTokens, systemThemeTokens, toThemeStyle } from './theme.js';
8
10
  import { Data } from '@puckeditor/core';
9
11
  export { WebRenderer, WebRendererProps } from './renderer.js';
10
- export { maximumBlockInstances, mergeBlockPolicies, minimumBlockInstances, validateBlockPolicy, validateFieldValue, validatePageDocumentWithRegistry } from './extensions.js';
11
12
 
12
13
  type BlockView = "blocks" | "outline";
13
14
  type Device = "desktop" | "tablet" | "mobile" | "full";
@@ -129,6 +130,19 @@ type AssetPickerAdapter = {
129
130
  current?: Partial<AssetReference>;
130
131
  }): Promise<AssetReference | null>;
131
132
  };
133
+ /**
134
+ * Opens a host-owned product picker. Search, Shopify App Bridge, and
135
+ * authorization stay in the host; this package only displays the snapshot
136
+ * and forwards the button click.
137
+ */
138
+ type ProductPickerAdapter = {
139
+ selectProducts(input: {
140
+ pageId: string;
141
+ blockId: string;
142
+ current: readonly ProductReference[];
143
+ multiple?: boolean;
144
+ }): Promise<readonly ProductReference[] | null>;
145
+ };
132
146
  type PagePublicationStatus = "unpublished" | "published";
133
147
  /** Presentational data for the reusable page state card. */
134
148
  type PageStatus = {
@@ -233,9 +247,16 @@ type PageDocumentEditorShellProps = {
233
247
  publishAction?: PublishAction;
234
248
  /** Host-owned picker/uploader for shop-scoped assets. */
235
249
  assetPicker?: AssetPickerAdapter;
250
+ /** Host-owned product picker. The shell only shows the selected snapshot and a button. */
251
+ productPicker?: ProductPickerAdapter;
236
252
  /** Optional reusable page lifecycle summary. */
237
253
  pageStatus?: PageStatus;
238
254
  onBack?: () => void;
255
+ onHistory?: (input: {
256
+ document: PageDocument;
257
+ draftRevision?: number;
258
+ session?: EditorSession;
259
+ }) => void;
239
260
  onPreview?: (input: {
240
261
  document: PageDocument;
241
262
  draftRevision?: number;
@@ -286,6 +307,7 @@ declare const messages: {
286
307
  readonly unsaved: "未保存变更";
287
308
  readonly saved: "所有更改已保留在当前会话中";
288
309
  readonly save: "保存草稿";
310
+ readonly history: "历史记录";
289
311
  readonly saving: "正在保存…";
290
312
  readonly publish: "发布";
291
313
  readonly publishing: "正在发布…";
@@ -314,9 +336,11 @@ declare const messages: {
314
336
  readonly collapseRight: "收起属性面板";
315
337
  readonly expandLeft: "打开区块面板";
316
338
  readonly expandRight: "打开属性面板";
317
- readonly toggleSidebars: "侧栏显示";
318
- readonly toggleLeftSidebar: "切换左侧面板";
319
- readonly toggleRightSidebar: "切换属性面板";
339
+ readonly selectProducts: "从 Shopify 选择商品";
340
+ readonly selectingProducts: "正在选择商品…";
341
+ readonly selectProductsFailed: "无法选择商品,请重试。";
342
+ readonly emptyProducts: "未选择商品";
343
+ readonly productCountSuffix: "件商品";
320
344
  };
321
345
  readonly en: {
322
346
  readonly blocks: "Blocks";
@@ -341,6 +365,7 @@ declare const messages: {
341
365
  readonly unsaved: "Unsaved changes";
342
366
  readonly saved: "Changes are kept in this session";
343
367
  readonly save: "Save draft";
368
+ readonly history: "History";
344
369
  readonly saving: "Saving…";
345
370
  readonly publish: "Publish";
346
371
  readonly publishing: "Publishing…";
@@ -369,19 +394,21 @@ declare const messages: {
369
394
  readonly collapseRight: "Collapse properties panel";
370
395
  readonly expandLeft: "Open blocks panel";
371
396
  readonly expandRight: "Open properties panel";
372
- readonly toggleSidebars: "Sidebar visibility";
373
- readonly toggleLeftSidebar: "Toggle left sidebar";
374
- readonly toggleRightSidebar: "Toggle right sidebar";
397
+ readonly selectProducts: "Select from Shopify";
398
+ readonly selectingProducts: "Selecting products…";
399
+ readonly selectProductsFailed: "Could not select products. Try again.";
400
+ readonly emptyProducts: "No products selected";
401
+ readonly productCountSuffix: "products";
375
402
  };
376
403
  };
377
404
  type AdminMessageKey = keyof typeof messages["zh-CN"];
378
405
  /** Minimal, dependency-free Admin copy catalog. Product locale remains PageDocument.settings.locale. */
379
406
  declare function createAdminI18n(locale?: string): {
380
407
  locale: AdminLocale;
381
- t: (key: AdminMessageKey) => "Desktop" | "Tablet" | "Mobile" | "未保存变更" | "Outline" | "Preview" | "缩放" | "区块" | "结构" | "撤销" | "恢复" | "收起左侧面板" | "收起属性面板" | "Delete" | "Duplicate" | "编辑锁已失效" | "属性" | "添加区块" | "复制" | "删除" | "确认删除区块?" | "删除后可使用撤销恢复。" | "取消" | "确认删除" | "上移" | "下移" | "正在加载编辑器…" | "页面还没有区块" | "编辑器无法加载" | "编辑器当前不可编辑" | "操作已完成" | "所有更改已保留在当前会话中" | "保存草稿" | "正在保存…" | "发布" | "正在发布…" | "保存草稿失败" | "重试保存" | "当前离线,恢复网络后将自动保存" | "发布失败" | "已发布" | "返回" | "离开编辑器?" | "离开" | "自动缩放" | "正式预览" | "添加到店铺" | "正在获取编辑锁…" | "该页面正在被编辑" | "页面为只读状态" | "你有未保存的更改。确定要离开吗?" | "选择一个区块以编辑。" | "桌面" | "平板" | "手机" | "打开区块面板" | "打开属性面板" | "侧栏显示" | "切换左侧面板" | "切换属性面板" | "Blocks" | "Properties" | "Add block" | "Undo" | "Redo" | "Delete this block?" | "You can restore it with Undo after deleting." | "Cancel" | "Delete block" | "Move up" | "Move down" | "Loading editor…" | "This page has no blocks" | "The editor could not load" | "The editor is currently read-only" | "Action completed" | "Unsaved changes" | "Changes are kept in this session" | "Save draft" | "Saving…" | "Publish" | "Publishing…" | "Could not save draft" | "Retry save" | "Offline — changes will save when connection returns" | "Could not publish" | "Published" | "Back" | "Leave the editor?" | "Leave" | "Zoom" | "Auto zoom" | "Add to store" | "Acquiring edit lock…" | "This page is being edited" | "The edit lock has expired" | "This page is read-only" | "You have unsaved changes. Are you sure you want to leave?" | "Select a block to edit it." | "Collapse blocks panel" | "Collapse properties panel" | "Open blocks panel" | "Open properties panel" | "Sidebar visibility" | "Toggle left sidebar" | "Toggle right sidebar";
408
+ t: (key: AdminMessageKey) => "Desktop" | "Tablet" | "Mobile" | "未保存变更" | "Outline" | "Preview" | "缩放" | "区块" | "结构" | "撤销" | "恢复" | "收起左侧面板" | "收起属性面板" | "Delete" | "Duplicate" | "products" | "编辑锁已失效" | "属性" | "添加区块" | "复制" | "删除" | "确认删除区块?" | "删除后可使用撤销恢复。" | "取消" | "确认删除" | "上移" | "下移" | "正在加载编辑器…" | "页面还没有区块" | "编辑器无法加载" | "编辑器当前不可编辑" | "操作已完成" | "所有更改已保留在当前会话中" | "保存草稿" | "历史记录" | "正在保存…" | "发布" | "正在发布…" | "保存草稿失败" | "重试保存" | "当前离线,恢复网络后将自动保存" | "发布失败" | "已发布" | "返回" | "离开编辑器?" | "离开" | "自动缩放" | "正式预览" | "添加到店铺" | "正在获取编辑锁…" | "该页面正在被编辑" | "页面为只读状态" | "你有未保存的更改。确定要离开吗?" | "选择一个区块以编辑。" | "桌面" | "平板" | "手机" | "打开区块面板" | "打开属性面板" | "从 Shopify 选择商品" | "正在选择商品…" | "无法选择商品,请重试。" | "未选择商品" | "件商品" | "Blocks" | "Properties" | "Add block" | "Undo" | "Redo" | "Delete this block?" | "You can restore it with Undo after deleting." | "Cancel" | "Delete block" | "Move up" | "Move down" | "Loading editor…" | "This page has no blocks" | "The editor could not load" | "The editor is currently read-only" | "Action completed" | "Unsaved changes" | "Changes are kept in this session" | "Save draft" | "History" | "Saving…" | "Publish" | "Publishing…" | "Could not save draft" | "Retry save" | "Offline — changes will save when connection returns" | "Could not publish" | "Published" | "Back" | "Leave the editor?" | "Leave" | "Zoom" | "Auto zoom" | "Add to store" | "Acquiring edit lock…" | "This page is being edited" | "The edit lock has expired" | "This page is read-only" | "You have unsaved changes. Are you sure you want to leave?" | "Select a block to edit it." | "Collapse blocks panel" | "Collapse properties panel" | "Open blocks panel" | "Open properties panel" | "Select from Shopify" | "Selecting products…" | "Could not select products. Try again." | "No products selected";
382
409
  };
383
410
 
384
411
  declare function toEngineData(document: PageDocument, registry?: ExtensionRegistry): Data;
385
412
  declare function fromEngineData(data: Data, base: PageDocument, registry?: ExtensionRegistry): PageDocument;
386
413
 
387
- export { type AdminLocale, type AdminMessageKey, type AssetPickerAdapter, type AssetReference, type AssetStatus, BlockNode, BlockPolicy, type DeleteConfirmationConfig, type DemoBlock, type Device, type DraftPersistenceAdapter, type DraftSaveInput, type DraftSaveResult, type EditorActionState, type EditorLoadState, EditorOperationPolicy, EditorProvider, type EditorSession, type EditorSessionAcquireResult, type EditorSessionAdapter, type EditorSessionState, EditorShell, type EditorShellProps, type EditorState, ExtensionRegistry, JsonValue, PageDocument, type PageDocumentEditorPolicy, PageDocumentEditorShell, type PageDocumentEditorShellProps, type PagePublicationStatus, type PageStatus, PageStatusCard, type PageStatusCardProps, type PreviewMode, type PublishAction, type PublishActionInput, type PublishActionResult, type PublishState, type SaveState, ValidationIssue, type Zoom, createAdminI18n, fromEngineData, toEngineData, useEditorContext };
414
+ export { type AdminLocale, type AdminMessageKey, type AssetPickerAdapter, type AssetReference, type AssetStatus, BlockNode, BlockPolicy, type DeleteConfirmationConfig, type DemoBlock, type Device, type DraftPersistenceAdapter, type DraftSaveInput, type DraftSaveResult, type EditorActionState, type EditorLoadState, EditorOperationPolicy, EditorProvider, type EditorSession, type EditorSessionAcquireResult, type EditorSessionAdapter, type EditorSessionState, EditorShell, type EditorShellProps, type EditorState, ExtensionRegistry, JsonValue, PageDocument, type PageDocumentEditorPolicy, PageDocumentEditorShell, type PageDocumentEditorShellProps, type PagePublicationStatus, type PageStatus, PageStatusCard, type PageStatusCardProps, type PreviewMode, type ProductPickerAdapter, ProductReference, type PublishAction, type PublishActionInput, type PublishActionResult, type PublishState, type SaveState, ValidationIssue, type Zoom, createAdminI18n, fromEngineData, toEngineData, useEditorContext };
package/dist/index.js CHANGED
@@ -4,13 +4,16 @@ import {
4
4
  TemplateRegistry,
5
5
  createExtensionRegistry,
6
6
  createTemplateRegistry,
7
+ isProductReference,
7
8
  maximumBlockInstances,
8
9
  mergeBlockPolicies,
9
10
  minimumBlockInstances,
11
+ parseProductReferences,
12
+ toProductReferenceJson,
10
13
  validateBlockPolicy,
11
14
  validateFieldValue,
12
15
  validatePageDocumentWithRegistry
13
- } from "./chunk-PFCWXLWN.js";
16
+ } from "./chunk-YXBTNYPR.js";
14
17
  import {
15
18
  WebRenderer
16
19
  } from "./chunk-KYXIXIWK.js";
@@ -347,9 +350,9 @@ function PropertyPanel({
347
350
  }
348
351
 
349
352
  // src/editor/shell/PageDocumentEditorShell.tsx
350
- import { IconButton as IconButton2, Puck as Puck2, usePuck } from "@puckeditor/core";
353
+ import { Puck as Puck2, usePuck } from "@puckeditor/core";
351
354
  import { Badge as Badge3, Banner, Button as Button2, ButtonGroup as ButtonGroup2, InlineStack as InlineStack3, Select as Select2, Text as Text3, TextField } from "@shopify/polaris";
352
- import { DragHandleIcon as DragHandleIcon2, LayoutSectionIcon as LayoutSectionIcon2, MenuIcon as MenuIcon2, RedoIcon as RedoIcon2, UndoIcon as UndoIcon2, XIcon as XIcon2 } from "@shopify/polaris-icons";
355
+ import { DragHandleIcon as DragHandleIcon2, LayoutSectionIcon as LayoutSectionIcon2, MenuIcon as MenuIcon2, ProductIcon, RedoIcon as RedoIcon2, UndoIcon as UndoIcon2, XIcon as XIcon2 } from "@shopify/polaris-icons";
353
356
  import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef4, useState as useState3 } from "react";
354
357
 
355
358
  // src/adapters/puck/canvas-extension-block.tsx
@@ -786,6 +789,7 @@ var messages = {
786
789
  unsaved: "\u672A\u4FDD\u5B58\u53D8\u66F4",
787
790
  saved: "\u6240\u6709\u66F4\u6539\u5DF2\u4FDD\u7559\u5728\u5F53\u524D\u4F1A\u8BDD\u4E2D",
788
791
  save: "\u4FDD\u5B58\u8349\u7A3F",
792
+ history: "\u5386\u53F2\u8BB0\u5F55",
789
793
  saving: "\u6B63\u5728\u4FDD\u5B58\u2026",
790
794
  publish: "\u53D1\u5E03",
791
795
  publishing: "\u6B63\u5728\u53D1\u5E03\u2026",
@@ -814,9 +818,11 @@ var messages = {
814
818
  collapseRight: "\u6536\u8D77\u5C5E\u6027\u9762\u677F",
815
819
  expandLeft: "\u6253\u5F00\u533A\u5757\u9762\u677F",
816
820
  expandRight: "\u6253\u5F00\u5C5E\u6027\u9762\u677F",
817
- toggleSidebars: "\u4FA7\u680F\u663E\u793A",
818
- toggleLeftSidebar: "\u5207\u6362\u5DE6\u4FA7\u9762\u677F",
819
- toggleRightSidebar: "\u5207\u6362\u5C5E\u6027\u9762\u677F"
821
+ selectProducts: "\u4ECE Shopify \u9009\u62E9\u5546\u54C1",
822
+ selectingProducts: "\u6B63\u5728\u9009\u62E9\u5546\u54C1\u2026",
823
+ selectProductsFailed: "\u65E0\u6CD5\u9009\u62E9\u5546\u54C1\uFF0C\u8BF7\u91CD\u8BD5\u3002",
824
+ emptyProducts: "\u672A\u9009\u62E9\u5546\u54C1",
825
+ productCountSuffix: "\u4EF6\u5546\u54C1"
820
826
  },
821
827
  en: {
822
828
  blocks: "Blocks",
@@ -841,6 +847,7 @@ var messages = {
841
847
  unsaved: "Unsaved changes",
842
848
  saved: "Changes are kept in this session",
843
849
  save: "Save draft",
850
+ history: "History",
844
851
  saving: "Saving\u2026",
845
852
  publish: "Publish",
846
853
  publishing: "Publishing\u2026",
@@ -869,9 +876,11 @@ var messages = {
869
876
  collapseRight: "Collapse properties panel",
870
877
  expandLeft: "Open blocks panel",
871
878
  expandRight: "Open properties panel",
872
- toggleSidebars: "Sidebar visibility",
873
- toggleLeftSidebar: "Toggle left sidebar",
874
- toggleRightSidebar: "Toggle right sidebar"
879
+ selectProducts: "Select from Shopify",
880
+ selectingProducts: "Selecting products\u2026",
881
+ selectProductsFailed: "Could not select products. Try again.",
882
+ emptyProducts: "No products selected",
883
+ productCountSuffix: "products"
875
884
  }
876
885
  };
877
886
  function createAdminI18n(locale) {
@@ -899,12 +908,6 @@ function blockTypeLabel(type, registry) {
899
908
  if (type === "core.image") return "\u56FE\u7247";
900
909
  return registry?.getBlock(type)?.label ?? type;
901
910
  }
902
- function PuckPanelIcon({ side }) {
903
- return /* @__PURE__ */ jsxs5("svg", { xmlns: "http://www.w3.org/2000/svg", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: [
904
- /* @__PURE__ */ jsx7("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
905
- /* @__PURE__ */ jsx7("path", { d: side === "left" ? "M9 3v18" : "M15 3v18" })
906
- ] });
907
- }
908
911
  function validateDocumentBlocks(document, registry) {
909
912
  return registry ? validatePageDocumentWithRegistry(document, registry) : [];
910
913
  }
@@ -957,7 +960,7 @@ function PageDocumentEditorShell(props) {
957
960
  }, [managedSession.state, onSessionStateChange]);
958
961
  return /* @__PURE__ */ jsx7(EditorProvider, { initialDocument: props.initialDocument, registry: props.registry, policy: props.policy, loadState: props.loadState, sessionState: managedSession.state, session: managedSession.session, sessionMessage: managedSession.message, leaveWarning: createAdminI18n(props.adminLocale).t("leaveWarning"), onDocumentChange: props.onDocumentChange, children: /* @__PURE__ */ jsx7(PageDocumentEditor, { ...props }) });
959
962
  }
960
- function PageDocumentEditor({ iframe = false, registry, adminLocale, onSave, onPublish, draftPersistence, draftRevision: initialDraftRevision, autoSave = true, autoSaveDelayMs = 800, publishAction, assetPicker, pageStatus, onBack, onPreview, onAddToStore, deleteConfirmation, availableBlockTypes, appearanceControls = false }) {
963
+ function PageDocumentEditor({ iframe = false, registry, adminLocale, onSave, onPublish, draftPersistence, draftRevision: initialDraftRevision, autoSave = true, autoSaveDelayMs = 800, publishAction, assetPicker, productPicker, pageStatus, onBack, onHistory, onPreview, onAddToStore, deleteConfirmation, availableBlockTypes, appearanceControls = false }) {
961
964
  const editor = useEditorContext();
962
965
  const [blockView, setBlockView] = useState3("blocks");
963
966
  const [leftRailOpen, setLeftRailOpen] = useState3(true);
@@ -1104,18 +1107,15 @@ function PageDocumentEditor({ iframe = false, registry, adminLocale, onSave, onP
1104
1107
  editor.document.target
1105
1108
  ] }),
1106
1109
  pageStatus ? /* @__PURE__ */ jsx7(PageStatusCard, { status: pageStatus, sessionState: editor.sessionState }) : null
1107
- ] }),
1108
- /* @__PURE__ */ jsxs5("div", { className: "pb-header-sidebar-toggles", role: "group", "aria-label": i18n.t("toggleSidebars"), "data-testid": "sidebar-toggles", children: [
1109
- /* @__PURE__ */ jsx7(IconButton2, { type: "button", title: i18n.t("toggleLeftSidebar"), onClick: () => setLeftRailOpen((open) => !open), children: /* @__PURE__ */ jsx7(PuckPanelIcon, { side: "left" }) }),
1110
- /* @__PURE__ */ jsx7(IconButton2, { type: "button", title: i18n.t("toggleRightSidebar"), onClick: () => setRightPanelOpen((open) => !open), children: /* @__PURE__ */ jsx7(PuckPanelIcon, { side: "right" }) })
1111
1110
  ] })
1112
1111
  ] }),
1113
- /* @__PURE__ */ jsxs5("div", { className: "pb-header-device-toolbar", children: [
1112
+ /* @__PURE__ */ jsx7("div", { className: "pb-header-controls", children: /* @__PURE__ */ jsxs5("div", { className: "pb-header-device-toolbar", children: [
1114
1113
  /* @__PURE__ */ jsx7(ButtonGroup2, { variant: "segmented", children: Object.keys(deviceLabels2).map((device) => /* @__PURE__ */ jsx7(Button2, { pressed: editor.device === device, onClick: () => editor.setDevice(device), children: i18n.t(deviceLabels2[device]) }, device)) }),
1115
1114
  /* @__PURE__ */ jsx7("div", { className: "pb-zoom-control", children: /* @__PURE__ */ jsx7(Select2, { label: i18n.t("zoom"), labelHidden: true, options: [{ label: i18n.t("zoomAuto"), value: "auto" }, { label: "50%", value: "50" }, { label: "70%", value: "70" }, { label: "100%", value: "100" }], value: zoom, onChange: (value) => setZoom(value) }) })
1116
- ] }),
1117
- /* @__PURE__ */ jsx7("div", { className: "pb-header-actions", children: /* @__PURE__ */ jsxs5(InlineStack3, { gap: "150", blockAlign: "center", wrap: false, children: [
1115
+ ] }) }),
1116
+ /* @__PURE__ */ jsx7("div", { className: "pb-header-actions", children: /* @__PURE__ */ jsxs5(InlineStack3, { gap: "150", blockAlign: "center", wrap: true, children: [
1118
1117
  /* @__PURE__ */ jsx7(Badge3, { tone: saveBadgeTone, children: saveBadgeLabel }),
1118
+ onHistory ? /* @__PURE__ */ jsx7("span", { "data-editor-history": "", children: /* @__PURE__ */ jsx7(Button2, { disabled: request !== "idle", onClick: () => onHistory({ document: editor.document, draftRevision, session: editor.session }), children: i18n.t("history") }) }) : null,
1119
1119
  /* @__PURE__ */ jsx7(Button2, { disabled: !canPersistDraft || !editor.isDirty || request !== "idle" || validationIssues.length > 0 || !isOnline, onClick: () => void save(), children: saveButtonLabel }),
1120
1120
  onPreview ? /* @__PURE__ */ jsx7(Button2, { disabled: request !== "idle" || !isOnline, onClick: () => void onPreview({ document: editor.document, draftRevision, session: editor.session }), children: i18n.t("preview") }) : null,
1121
1121
  onAddToStore ? /* @__PURE__ */ jsx7(Button2, { disabled: request !== "idle" || pageStatus?.publicationStatus === "unpublished", onClick: () => void onAddToStore({ document: editor.document, session: editor.session }), children: i18n.t("addToStore") }) : null,
@@ -1189,7 +1189,7 @@ function PageDocumentEditor({ iframe = false, registry, adminLocale, onSave, onP
1189
1189
  /* @__PURE__ */ jsx7(Button2, { accessibilityLabel: i18n.t("collapseRight"), icon: XIcon2, variant: "tertiary", onClick: () => setRightPanelOpen(false) })
1190
1190
  ] }),
1191
1191
  editor.selectedBlock ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1192
- /* @__PURE__ */ jsx7(DocumentInspector, { pageId: editor.document.pageId, assetPicker, block: editor.selectedBlock, registry, disabled: !editor.actionState.canEdit, appearanceControls, onChange: (props) => editor.updateBlockProps(editor.selectedBlock.id, props), onPresentationChange: (presentation) => updateBlockPresentation(editor.selectedBlock.id, presentation) }),
1192
+ /* @__PURE__ */ jsx7(DocumentInspector, { pageId: editor.document.pageId, assetPicker, productPicker, i18n, block: editor.selectedBlock, registry, disabled: !editor.actionState.canEdit, appearanceControls, onChange: (props) => editor.updateBlockProps(editor.selectedBlock.id, props), onPresentationChange: (presentation) => updateBlockPresentation(editor.selectedBlock.id, presentation) }),
1193
1193
  /* @__PURE__ */ jsx7(InspectorActions, { block: editor.selectedBlock, canDuplicate: editor.actionState.canDuplicate, canDelete: editor.actionState.canDelete, canMove: editor.actionState.canReorder, canMoveUp: editor.document.blocks[0]?.id !== editor.selectedBlock.id, canMoveDown: editor.document.blocks.at(-1)?.id !== editor.selectedBlock.id, i18n, onDuplicate: () => editor.duplicateBlock(editor.selectedBlock.id), onDelete: () => editor.requestDeleteBlock(editor.selectedBlock.id), onMove: (direction) => editor.moveBlock(editor.selectedBlock.id, direction) })
1194
1194
  ] }) : /* @__PURE__ */ jsx7(Text3, { as: "p", tone: "subdued", children: i18n.t("selectBlock") })
1195
1195
  ] })
@@ -1260,7 +1260,33 @@ function InspectorTextControl({ label, value, control, disabled, onChange }) {
1260
1260
  if (control === "color") return /* @__PURE__ */ jsx7("input", { "aria-label": label, type: "color", value: /^#[\da-f]{6}$/i.test(stringValue) ? stringValue : "#000000", disabled, onChange: (event) => onChange(event.currentTarget.value) });
1261
1261
  return /* @__PURE__ */ jsx7(TextField, { label, labelHidden: true, value: stringValue, onChange, autoComplete: "off", disabled, multiline: control === "textarea" ? 4 : false, type: control === "url" ? "url" : "text" });
1262
1262
  }
1263
- function InspectorField({ name, field, value, registry, disabled, onChange }) {
1263
+ function InspectorProductsControl({ value, disabled, picker, pageId, blockId, i18n, onChange }) {
1264
+ const products = parseProductReferences(value);
1265
+ const [busy, setBusy] = useState3(false);
1266
+ const [error, setError] = useState3(null);
1267
+ const preview = products.slice(0, 4);
1268
+ return /* @__PURE__ */ jsxs5("div", { className: "pb-product-picker", "data-testid": "product-picker", children: [
1269
+ /* @__PURE__ */ jsxs5("div", { className: "pb-product-picker__summary", children: [
1270
+ preview.length ? /* @__PURE__ */ jsx7("span", { className: "pb-product-picker__thumbs", "aria-hidden": "true", children: preview.map((product, index) => /* @__PURE__ */ jsx7("span", { className: "pb-product-picker__thumb", style: { zIndex: preview.length - index }, children: product.imageUrl ? /* @__PURE__ */ jsx7("img", { src: product.imageUrl, alt: "" }) : /* @__PURE__ */ jsx7("span", { className: "pb-product-picker__thumb-fallback" }) }, product.id)) }) : null,
1271
+ /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", children: products.length ? `${products.length} ${i18n.t("productCountSuffix")}` : i18n.t("emptyProducts") })
1272
+ ] }),
1273
+ /* @__PURE__ */ jsx7(Button2, { fullWidth: true, icon: ProductIcon, variant: "secondary", disabled: disabled || busy || !picker, onClick: () => void (async () => {
1274
+ if (!picker) return;
1275
+ setBusy(true);
1276
+ setError(null);
1277
+ try {
1278
+ const next = await picker.selectProducts({ pageId, blockId, current: products, multiple: true });
1279
+ if (next) onChange(parseProductReferences(next).map(toProductReferenceJson));
1280
+ } catch {
1281
+ setError(i18n.t("selectProductsFailed"));
1282
+ } finally {
1283
+ setBusy(false);
1284
+ }
1285
+ })(), children: busy ? i18n.t("selectingProducts") : i18n.t("selectProducts") }),
1286
+ error ? /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "critical", children: error }) : null
1287
+ ] });
1288
+ }
1289
+ function InspectorField({ name, field, value, registry, disabled, productPicker, pageId, blockId, i18n, onChange }) {
1264
1290
  const label = field.label ?? name;
1265
1291
  const issues = validateFieldValue(field, value);
1266
1292
  const Field = registry?.getField(field.field)?.component;
@@ -1269,7 +1295,7 @@ function InspectorField({ name, field, value, registry, disabled, onChange }) {
1269
1295
  /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", fontWeight: "semibold", children: label }),
1270
1296
  field.description ? /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "subdued", children: field.description }) : null
1271
1297
  ] }),
1272
- field.control ? /* @__PURE__ */ jsx7(InspectorTextControl, { label, value, control: field.control, disabled, onChange: (next) => onChange(next) }) : Field ? /* @__PURE__ */ jsx7(Field, { value, onChange }) : null,
1298
+ field.control === "products" ? /* @__PURE__ */ jsx7(InspectorProductsControl, { value, disabled, picker: productPicker, pageId, blockId, i18n, onChange }) : field.control ? /* @__PURE__ */ jsx7(InspectorTextControl, { label, value, control: field.control, disabled, onChange: (next) => onChange(next) }) : Field ? /* @__PURE__ */ jsx7(Field, { value, onChange }) : null,
1273
1299
  issues.map((issue) => /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "critical", children: issue.message }, issue.message))
1274
1300
  ] });
1275
1301
  }
@@ -1308,7 +1334,7 @@ function inspectorFieldConfig(name, field) {
1308
1334
  return { ...field, group: field.group ?? group, description };
1309
1335
  }
1310
1336
  var appearanceTokens = [["color.primary", "\u4E3B\u8272"], ["color.surface", "\u8868\u9762\u8272"], ["radius", "\u5706\u89D2"], ["spacing", "\u95F4\u8DDD"]];
1311
- function DocumentInspector({ pageId, assetPicker, block, registry, disabled, appearanceControls, onChange, onPresentationChange }) {
1337
+ function DocumentInspector({ pageId, assetPicker, productPicker, i18n, block, registry, disabled, appearanceControls, onChange, onPresentationChange }) {
1312
1338
  const [assetError, setAssetError] = useState3(null);
1313
1339
  const [selectingAsset, setSelectingAsset] = useState3(false);
1314
1340
  const definition = registry?.getBlock(block.type);
@@ -1327,10 +1353,10 @@ function DocumentInspector({ pageId, assetPicker, block, registry, disabled, app
1327
1353
  /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "subdued", children: definition?.category ?? "Core block" })
1328
1354
  ] })
1329
1355
  ] }),
1330
- block.type === "core.text" ? /* @__PURE__ */ jsx7(InspectorSection, { title: "Content", children: /* @__PURE__ */ jsx7(InspectorField, { name: "content", field: { field: "", label: "\u6587\u672C\u5185\u5BB9", control: "textarea", description: "\u652F\u6301\u8F83\u957F\u7684\u6B63\u6587\u5185\u5BB9\u3002" }, value: block.props.content, registry, disabled, onChange: (content) => onChange({ content }) }) }) : null,
1356
+ block.type === "core.text" ? /* @__PURE__ */ jsx7(InspectorSection, { title: "Content", children: /* @__PURE__ */ jsx7(InspectorField, { name: "content", field: { field: "", label: "\u6587\u672C\u5185\u5BB9", control: "textarea", description: "\u652F\u6301\u8F83\u957F\u7684\u6B63\u6587\u5185\u5BB9\u3002" }, value: block.props.content, registry, disabled, productPicker, pageId, blockId: block.id, i18n, onChange: (content) => onChange({ content }) }) }) : null,
1331
1357
  block.type === "core.image" ? /* @__PURE__ */ jsxs5(InspectorSection, { title: "Image", children: [
1332
- /* @__PURE__ */ jsx7(InspectorField, { name: "src", field: { field: "", label: "\u56FE\u7247 URL", control: "url", description: "\u4F7F\u7528 HTTPS \u56FE\u7247\u5730\u5740\u3002" }, value: block.props.src, registry, disabled, onChange: (src) => onChange({ src }) }),
1333
- /* @__PURE__ */ jsx7(InspectorField, { name: "alt", field: { field: "", label: "\u66FF\u4EE3\u6587\u672C", control: "text", description: "\u7528\u4E8E\u65E0\u969C\u788D\u9605\u8BFB\u548C\u56FE\u7247\u52A0\u8F7D\u5931\u8D25\u573A\u666F\u3002" }, value: block.props.alt, registry, disabled, onChange: (alt) => onChange({ alt }) }),
1358
+ /* @__PURE__ */ jsx7(InspectorField, { name: "src", field: { field: "", label: "\u56FE\u7247 URL", control: "url", description: "\u4F7F\u7528 HTTPS \u56FE\u7247\u5730\u5740\u3002" }, value: block.props.src, registry, disabled, productPicker, pageId, blockId: block.id, i18n, onChange: (src) => onChange({ src }) }),
1359
+ /* @__PURE__ */ jsx7(InspectorField, { name: "alt", field: { field: "", label: "\u66FF\u4EE3\u6587\u672C", control: "text", description: "\u7528\u4E8E\u65E0\u969C\u788D\u9605\u8BFB\u548C\u56FE\u7247\u52A0\u8F7D\u5931\u8D25\u573A\u666F\u3002" }, value: block.props.alt, registry, disabled, productPicker, pageId, blockId: block.id, i18n, onChange: (alt) => onChange({ alt }) }),
1334
1360
  assetPicker ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1335
1361
  /* @__PURE__ */ jsx7(Button2, { disabled: disabled || selectingAsset, onClick: () => void (async () => {
1336
1362
  setSelectingAsset(true);
@@ -1347,7 +1373,7 @@ function DocumentInspector({ pageId, assetPicker, block, registry, disabled, app
1347
1373
  assetError ? /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "critical", children: assetError }) : null
1348
1374
  ] }) : null
1349
1375
  ] }) : null,
1350
- Object.entries(groupedFields).map(([group, fields]) => /* @__PURE__ */ jsx7(InspectorSection, { title: group, defaultOpen: group !== "Advanced", children: fields.map(([name, field]) => /* @__PURE__ */ jsx7(InspectorField, { name, field, value: block.props[name], registry, disabled, onChange: (value) => onChange({ ...block.props, [name]: value }) }, name)) }, group)),
1376
+ Object.entries(groupedFields).map(([group, fields]) => /* @__PURE__ */ jsx7(InspectorSection, { title: group, defaultOpen: group !== "Advanced", children: fields.map(([name, field]) => /* @__PURE__ */ jsx7(InspectorField, { name, field, value: block.props[name], registry, disabled, productPicker, pageId, blockId: block.id, i18n, onChange: (value) => onChange({ ...block.props, [name]: value }) }, name)) }, group)),
1351
1377
  appearanceControls && definition?.variants?.length ? /* @__PURE__ */ jsx7(InspectorSection, { title: "\u5916\u89C2", children: /* @__PURE__ */ jsx7(Select2, { label: "\u6837\u5F0F\u53D8\u4F53", options: definition.variants.map((variant) => ({ label: variant.label, value: variant.id })), value: block.variant, disabled, onChange: (variant) => onPresentationChange({ variant, style: block.style }) }) }) : null,
1352
1378
  appearanceControls && definition ? /* @__PURE__ */ jsx7(InspectorSection, { title: "\u6837\u5F0F\u8986\u76D6", defaultOpen: false, children: appearanceTokens.map(([token, label]) => /* @__PURE__ */ jsx7(TextField, { label, value: block.style[token] ?? "", placeholder: "\u7EE7\u627F\u9875\u9762\u6216\u6A21\u677F\u8BBE\u7F6E", autoComplete: "off", disabled, onChange: (value) => {
1353
1379
  const style = { ...block.style };
@@ -1371,14 +1397,17 @@ export {
1371
1397
  createPageDocument,
1372
1398
  createTemplateRegistry,
1373
1399
  fromEngineData,
1400
+ isProductReference,
1374
1401
  maximumBlockInstances,
1375
1402
  mergeBlockPolicies,
1376
1403
  mergeThemeTokens,
1377
1404
  migratePageDocument,
1378
1405
  minimumBlockInstances,
1379
1406
  normalizeThemeTokens,
1407
+ parseProductReferences,
1380
1408
  systemThemeTokens,
1381
1409
  toEngineData,
1410
+ toProductReferenceJson,
1382
1411
  toThemeStyle,
1383
1412
  useEditorContext,
1384
1413
  validateBlockPolicy,
@@ -42,7 +42,7 @@ type FieldConfig = {
42
42
  field: string;
43
43
  label?: string;
44
44
  /** Product-facing editor metadata. Custom field components remain supported. */
45
- control?: "text" | "textarea" | "url" | "color";
45
+ control?: "text" | "textarea" | "url" | "color" | "products";
46
46
  description?: string;
47
47
  group?: string;
48
48
  /** Kept for compatibility; it participates in the built-in validation. */
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { h as ExtensionRegistry } from './registry-eNtkEC6o.js';
2
+ import { h as ExtensionRegistry } from './registry-CXJdOya3.js';
3
3
  import { PageDocument } from './schema.js';
4
4
  import './theme.js';
5
5
 
package/dist/runtime.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { WebRenderer, WebRendererProps } from './renderer.js';
2
- export { B as BlockDefinition, a as BlockEditorProps, b as BlockOperation, c as BlockPolicy, d as BlockVariantDefinition, D as DataSourceDefinition, E as EditorAction, e as EditorActionPosition, f as EditorOperationPolicy, g as ExtensionActionContext, h as ExtensionRegistry, i as ExtensionRegistryError, j as ExtensionRegistryErrorCode, k as ExtensionRegistryOptions, l as ExtensionTarget, F as FieldConfig, m as FieldDefinition, n as FieldProps, o as FieldValidation, L as LifecycleHooks, P as PageBuilderExtension, R as RendererDefinition, T as TemplateDefinition, p as TemplateRegistry, q as TemplateSource, U as UISlotContribution, r as UISlotName, V as ValidationIssue, s as createExtensionRegistry, t as createTemplateRegistry } from './registry-eNtkEC6o.js';
3
- export { maximumBlockInstances, mergeBlockPolicies, minimumBlockInstances, validateBlockPolicy, validateFieldValue, validatePageDocumentWithRegistry } from './extensions.js';
2
+ export { B as BlockDefinition, a as BlockEditorProps, b as BlockOperation, c as BlockPolicy, d as BlockVariantDefinition, D as DataSourceDefinition, E as EditorAction, e as EditorActionPosition, f as EditorOperationPolicy, g as ExtensionActionContext, h as ExtensionRegistry, i as ExtensionRegistryError, j as ExtensionRegistryErrorCode, k as ExtensionRegistryOptions, l as ExtensionTarget, F as FieldConfig, m as FieldDefinition, n as FieldProps, o as FieldValidation, L as LifecycleHooks, P as PageBuilderExtension, R as RendererDefinition, T as TemplateDefinition, p as TemplateRegistry, q as TemplateSource, U as UISlotContribution, r as UISlotName, V as ValidationIssue, s as createExtensionRegistry, t as createTemplateRegistry } from './registry-CXJdOya3.js';
3
+ export { ProductReference, isProductReference, maximumBlockInstances, mergeBlockPolicies, minimumBlockInstances, parseProductReferences, toProductReferenceJson, validateBlockPolicy, validateFieldValue, validatePageDocumentWithRegistry } from './extensions.js';
4
4
  export { BlockNode, BlockStyleOverrides, DataBinding, JsonPrimitive, JsonValue, PageDocument, PageDocumentIssue, PageDocumentMigration, PageDocumentSchemaVersion, PageDocumentValidation, PageSettings, RenderTarget, createPageDocument, migratePageDocument, validatePageDocument } from './schema.js';
5
5
  export { ThemeTokenName, ThemeTokenValidation, ThemeTokens, mergeThemeTokens, normalizeThemeTokens, systemThemeTokens, toThemeStyle } from './theme.js';
6
6
  import 'react';
package/dist/runtime.js CHANGED
@@ -4,13 +4,16 @@ import {
4
4
  TemplateRegistry,
5
5
  createExtensionRegistry,
6
6
  createTemplateRegistry,
7
+ isProductReference,
7
8
  maximumBlockInstances,
8
9
  mergeBlockPolicies,
9
10
  minimumBlockInstances,
11
+ parseProductReferences,
12
+ toProductReferenceJson,
10
13
  validateBlockPolicy,
11
14
  validateFieldValue,
12
15
  validatePageDocumentWithRegistry
13
- } from "./chunk-PFCWXLWN.js";
16
+ } from "./chunk-YXBTNYPR.js";
14
17
  import {
15
18
  WebRenderer
16
19
  } from "./chunk-KYXIXIWK.js";
@@ -33,13 +36,16 @@ export {
33
36
  createExtensionRegistry,
34
37
  createPageDocument,
35
38
  createTemplateRegistry,
39
+ isProductReference,
36
40
  maximumBlockInstances,
37
41
  mergeBlockPolicies,
38
42
  mergeThemeTokens,
39
43
  migratePageDocument,
40
44
  minimumBlockInstances,
41
45
  normalizeThemeTokens,
46
+ parseProductReferences,
42
47
  systemThemeTokens,
48
+ toProductReferenceJson,
43
49
  toThemeStyle,
44
50
  validateBlockPolicy,
45
51
  validateFieldValue,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@standhigher/puck-page-builder",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "React PageDocument editor, extension registry, and web renderer for BestTrack page experiences.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/styles.css CHANGED
@@ -7,16 +7,18 @@ button:focus-visible, [role="button"]:focus-visible { outline: 2px solid var(--b
7
7
  .pb-shell { min-height: 100vh; color: var(--builder-text); background: var(--builder-canvas-bg); }
8
8
  .pb-shell [class*="PuckLayout-inner"] { height: auto !important; min-height: 100%; }
9
9
  .pb-header { position: sticky; top: 0; z-index: 20; min-height: 56px; padding: var(--builder-space-2) var(--builder-space-4); border-bottom: 1px solid var(--builder-border); background: var(--builder-surface); }
10
- .pb-header-content { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: var(--builder-space-3); }
11
- .pb-header-title-group { display: flex; align-items: center; gap: var(--builder-space-2); min-width: 0; }
10
+ .pb-header-content { display: flex; flex-wrap: wrap; align-items: center; gap: var(--builder-space-2) var(--builder-space-3); }
11
+ .pb-header-title-group { display: flex; flex: 1 1 0; align-items: center; gap: var(--builder-space-2); min-width: 0; }
12
+ .pb-header-sidebar-toggles, [class*="PuckHeader-toggle"] { display: none !important; }
12
13
  .pb-page-title { display: grid; gap: 0; min-width: 0; }
13
- .pb-header-sidebar-toggles { display: flex; flex: 0 0 auto; align-items: center; gap: 2px; }
14
- .pb-header-sidebar-toggles [class*="IconButton"] { min-width: 28px; min-height: 28px; padding: 0; color: var(--builder-text); }
15
- .pb-header-sidebar-toggles svg { display: block; width: 28px; height: 28px; }
14
+ .pb-page-title h1, .pb-page-title p { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
16
15
  .pb-page-title p { margin: 0; }
17
- .pb-header-device-toolbar { display: flex; align-items: center; justify-self: center; gap: var(--builder-space-2); }
18
- .pb-header-actions { justify-self: end; min-width: 0; }
19
- .pb-zoom-control { width: 112px; }
16
+ .pb-header-controls { display: flex; flex: 0 0 auto; align-items: center; justify-content: center; }
17
+ .pb-header-device-toolbar { display: flex; align-items: center; gap: var(--builder-space-2); }
18
+ .pb-header-actions { display: flex; flex: 1 1 0; justify-content: flex-end; min-width: 0; }
19
+ .pb-header-actions .Polaris-InlineStack { flex-wrap: wrap; justify-content: flex-end; }
20
+ .pb-header-actions .Polaris-Badge { max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
21
+ .pb-zoom-control { width: 112px; flex: 0 0 auto; }
20
22
  .pb-page-status-card { display: grid; gap: 2px; margin-top: var(--builder-space-1); }
21
23
  .pb-page-status-card p { margin: 0; }
22
24
  .pb-workspace { display: grid; grid-template-columns: 64px minmax(260px, 300px) minmax(0, 1fr) minmax(300px, 320px); min-height: calc(100vh - 56px); }
@@ -50,6 +52,14 @@ button:focus-visible, [role="button"]:focus-visible { outline: 2px solid var(--b
50
52
  .pb-inspector-field input:hover, .pb-inspector-field textarea:hover { border-color: #94a3b8; }
51
53
  .pb-inspector-field input:focus, .pb-inspector-field textarea:focus { border-color: #2563eb; box-shadow: 0 0 0 3px rgb(37 99 235 / 12%); }
52
54
  .pb-inspector-field input:disabled, .pb-inspector-field textarea:disabled { color: #94a3b8; background: #f8fafc; cursor: not-allowed; }
55
+ .pb-product-picker { display: grid; gap: 8px; }
56
+ .pb-product-picker__summary { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 10px 12px; border: 1px solid #e2e8f0; border-radius: 10px; background: #fff; }
57
+ .pb-product-picker__summary p { margin: 0; color: #334155; }
58
+ .pb-product-picker__thumbs { display: flex; align-items: center; }
59
+ .pb-product-picker__thumb { display: inline-flex; width: 32px; height: 32px; margin-left: -8px; overflow: hidden; border: 2px solid #fff; border-radius: 8px; background: #e2e8f0; box-shadow: 0 0 0 1px #e2e8f0; }
60
+ .pb-product-picker__thumb:first-child { margin-left: 0; }
61
+ .pb-product-picker__thumb img, .pb-product-picker__thumb-fallback { display: block; width: 100%; height: 100%; object-fit: cover; }
62
+ .pb-product-picker__thumb-fallback { background: #cbd5e1; }
53
63
  .pb-inspector__hint { margin: 0; padding: 0 var(--builder-space-2); color: #64748b; font-size: 12px; line-height: 1.5; }
54
64
  .pb-inspector-actions { padding-top: var(--builder-space-2); border-top: 1px solid #e2e8f0; }
55
65
  .pb-delete-confirmation-backdrop { position: fixed; z-index: 1000; inset: 0; display: grid; place-items: center; padding: var(--builder-space-4); background: rgb(15 23 42 / 42%); }
@@ -98,7 +108,8 @@ button:focus-visible, [role="button"]:focus-visible { outline: 2px solid var(--b
98
108
  .pb-library-block-drag-hint svg { width: 16px; height: 16px; }
99
109
  .pb-document-block-row:hover { background: var(--builder-surface-subtle); }
100
110
  .pb-document-block-row--selected { border-color: var(--builder-accent); background: var(--builder-accent-subtle); }
101
- .pb-editor-status { display: grid; place-items: center; min-height: min(50vh, 420px); padding: var(--builder-space-6); }
111
+ .pb-editor-status { display: grid; place-items: center; box-sizing: border-box; width: 100%; min-height: 100dvh; padding: var(--builder-space-6); }
112
+ .pb-editor-status > * { width: min(100%, 28rem); justify-self: center; }
102
113
  .pb-shell--v04 > .Polaris-Banner { margin: var(--builder-space-3) var(--builder-space-4) 0; }
103
114
  .pb-document-canvas__text, .pb-document-canvas__image { max-width: 760px; margin: 0 auto; padding: 48px; color: #1d2939; background: #fff; cursor: pointer; }
104
115
  .pb-document-canvas__text { font-size: clamp(32px, 4vw, 52px); font-weight: 650; line-height: 1.1; letter-spacing: -.03em; }
@@ -131,5 +142,5 @@ button:focus-visible, [role="button"]:focus-visible { outline: 2px solid var(--b
131
142
  .bt-storefront__form input { flex: 1; min-width: 0; padding: 13px 14px; border: 1px solid #b8c3cc; border-radius: 8px; font: inherit; }
132
143
  .bt-storefront__form button { padding: 13px 18px; border: 0; border-radius: 8px; color: white; background: #203e54; font: inherit; font-weight: 600; }
133
144
  @media (max-width: 1180px) { .pb-workspace { grid-template-columns: 64px minmax(260px, 300px) minmax(0, 1fr); } .pb-workspace--left-closed { grid-template-columns: 64px minmax(0, 1fr); } .pb-right-panel { grid-column: 2 / -1; border-top: 1px solid var(--builder-border); border-left: 0; } }
134
- @media (max-width: 1023px) { .pb-header { position: static; overflow: auto; } .pb-workspace { grid-template-columns: 64px minmax(0, 1fr); } .pb-left-panel, .pb-right-panel { grid-column: 2; border-left: 0; border-right: 0; border-bottom: 1px solid var(--builder-border); } .pb-canvas-area { grid-column: 2; } }
135
- @media (max-width: 640px) { .pb-header, .pb-canvas-area { padding: var(--builder-space-2); } .pb-header-content { min-width: 680px; } .pb-canvas-stage { padding: 0; } .pb-canvas-frame--tablet, .pb-canvas-frame--mobile { width: 100%; } .bt-storefront { padding: 28px 20px; } .bt-storefront__form { flex-direction: column; } }
145
+ @media (max-width: 1023px) { .pb-header { position: static; } .pb-workspace { grid-template-columns: 64px minmax(0, 1fr); } .pb-left-panel, .pb-right-panel { grid-column: 2; border-left: 0; border-right: 0; border-bottom: 1px solid var(--builder-border); } .pb-canvas-area { grid-column: 2; } }
146
+ @media (max-width: 640px) { .pb-header, .pb-canvas-area { padding: var(--builder-space-2); } .pb-header-title-group, .pb-header-controls, .pb-header-actions { flex: 1 1 100%; } .pb-header-controls, .pb-header-actions { justify-content: flex-start; } .pb-header-actions .Polaris-InlineStack { justify-content: flex-start; } .pb-canvas-stage { padding: 0; } .pb-canvas-frame--tablet, .pb-canvas-frame--mobile { width: 100%; } .bt-storefront { padding: 28px 20px; } .bt-storefront__form { flex-direction: column; } }