@standhigher/puck-page-builder 0.11.0 → 0.12.1

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,13 +62,17 @@ 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" });
25
72
  if (control === "text" && /[\r\n]/.test(value)) issues.push({ path: "", message: "\u5FC5\u987B\u4E3A\u5355\u884C\u6587\u672C\u3002" });
26
73
  if (config.validation?.minLength !== void 0 && stringLength(value) < config.validation.minLength) issues.push({ path: "", message: `\u81F3\u5C11\u9700\u8981 ${config.validation.minLength} \u4E2A\u5B57\u7B26\u3002` });
27
74
  if (config.validation?.maxLength !== void 0 && stringLength(value) > config.validation.maxLength) issues.push({ path: "", message: `\u6700\u591A\u5141\u8BB8 ${config.validation.maxLength} \u4E2A\u5B57\u7B26\u3002` });
28
- if (value && control === "url" && !isSafeUrl(value, config)) issues.push({ path: "", message: "\u5FC5\u987B\u662F\u7AD9\u5185\u76F8\u5BF9\u8DEF\u5F84\u6216\u5141\u8BB8\u7684 HTTP(S) URL\u3002" });
75
+ if (value && (control === "url" || control === "asset") && !isSafeUrl(value, config)) issues.push({ path: "", message: "\u5FC5\u987B\u662F\u7AD9\u5185\u76F8\u5BF9\u8DEF\u5F84\u6216\u5141\u8BB8\u7684 HTTP(S) URL\u3002" });
29
76
  if (value && control === "color" && !hexColor.test(value)) issues.push({ path: "", message: "\u5FC5\u987B\u662F\u5341\u516D\u8FDB\u5236\u989C\u8272\uFF0C\u4F8B\u5982 #005BD3\u3002" });
30
77
  return issues;
31
78
  }
@@ -280,11 +327,12 @@ function validateBlock(document, index, registry) {
280
327
  if (block.version !== definition.version) issues.push({ path: `${path}.version`, message: `\u533A\u5757\u7248\u672C\u5FC5\u987B\u4E3A ${definition.version}\u3002` });
281
328
  if (!supportsTarget(definition, document.target)) issues.push({ path, message: `\u533A\u5757\u4E0D\u652F\u6301 ${document.target} target\u3002` });
282
329
  if (definition.variants?.length && !definition.variants.some((variant) => variant.id === block.variant)) issues.push({ path: `${path}.variant`, message: `\u4E0D\u652F\u6301\u7684 variant\uFF1A${block.variant}\u3002` });
283
- const allowedProps = /* @__PURE__ */ new Set([...Object.keys(definition.defaultProps), ...Object.keys(definition.fields)]);
330
+ const allowedProps = /* @__PURE__ */ new Set([...Object.keys(definition.defaultProps), ...Object.entries(definition.fields).filter(([, field]) => field.persist !== false).map(([name]) => name)]);
284
331
  for (const key of Object.keys(block.props)) {
285
332
  if (!allowedProps.has(key)) issues.push({ path: `${path}.props.${key}`, message: "\u4E0D\u5141\u8BB8\u4FDD\u5B58\u672A\u77E5\u5B57\u6BB5\u3002" });
286
333
  }
287
334
  for (const [name, field] of Object.entries(definition.fields)) {
335
+ if (field.persist === false) continue;
288
336
  for (const issue of validateFieldValue(field, block.props[name])) {
289
337
  issues.push({ ...issue, path: issue.path ? `${path}.props.${name}.${issue.path}` : `${path}.props.${name}` });
290
338
  }
@@ -314,6 +362,9 @@ function validatePageDocumentWithRegistry(document, registry) {
314
362
  }
315
363
 
316
364
  export {
365
+ isProductReference,
366
+ parseProductReferences,
367
+ toProductReferenceJson,
317
368
  validateFieldValue,
318
369
  validateBlockPolicy,
319
370
  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-BkqtMP-A.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-BkqtMP-A.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-RLKWR5JM.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-BkqtMP-A.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-BkqtMP-A.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,23 @@ type PageDocumentEditorShellProps = {
233
247
  publishAction?: PublishAction;
234
248
  /** Host-owned picker/uploader for shop-scoped assets. */
235
249
  assetPicker?: AssetPickerAdapter;
250
+ /** External settings, keyed by block id (or block type for shared settings), outside document history and persistence. */
251
+ inspectorSettings?: Record<string, {
252
+ values: Record<string, JsonValue>;
253
+ disabled?: boolean;
254
+ onChange: (fieldName: string, value: JsonValue) => void;
255
+ footer?: ReactNode;
256
+ }>;
257
+ /** Host-owned product picker. The shell only shows the selected snapshot and a button. */
258
+ productPicker?: ProductPickerAdapter;
236
259
  /** Optional reusable page lifecycle summary. */
237
260
  pageStatus?: PageStatus;
238
261
  onBack?: () => void;
262
+ onHistory?: (input: {
263
+ document: PageDocument;
264
+ draftRevision?: number;
265
+ session?: EditorSession;
266
+ }) => void;
239
267
  onPreview?: (input: {
240
268
  document: PageDocument;
241
269
  draftRevision?: number;
@@ -286,6 +314,7 @@ declare const messages: {
286
314
  readonly unsaved: "未保存变更";
287
315
  readonly saved: "所有更改已保留在当前会话中";
288
316
  readonly save: "保存草稿";
317
+ readonly history: "历史记录";
289
318
  readonly saving: "正在保存…";
290
319
  readonly publish: "发布";
291
320
  readonly publishing: "正在发布…";
@@ -314,9 +343,16 @@ declare const messages: {
314
343
  readonly collapseRight: "收起属性面板";
315
344
  readonly expandLeft: "打开区块面板";
316
345
  readonly expandRight: "打开属性面板";
317
- readonly toggleSidebars: "侧栏显示";
318
- readonly toggleLeftSidebar: "切换左侧面板";
319
- readonly toggleRightSidebar: "切换属性面板";
346
+ readonly selectProducts: "从 Shopify 选择商品";
347
+ readonly selectingProducts: "正在选择商品…";
348
+ readonly selectProductsFailed: "无法选择商品,请重试。";
349
+ readonly emptyProducts: "未选择商品";
350
+ readonly productCountSuffix: "件商品";
351
+ readonly selectAsset: "选择图片";
352
+ readonly selectingAsset: "正在上传图片…";
353
+ readonly selectAssetFailed: "无法上传图片,请重试。";
354
+ readonly emptyAsset: "未设置图片";
355
+ readonly removeAsset: "移除图片";
320
356
  };
321
357
  readonly en: {
322
358
  readonly blocks: "Blocks";
@@ -341,6 +377,7 @@ declare const messages: {
341
377
  readonly unsaved: "Unsaved changes";
342
378
  readonly saved: "Changes are kept in this session";
343
379
  readonly save: "Save draft";
380
+ readonly history: "History";
344
381
  readonly saving: "Saving…";
345
382
  readonly publish: "Publish";
346
383
  readonly publishing: "Publishing…";
@@ -369,19 +406,26 @@ declare const messages: {
369
406
  readonly collapseRight: "Collapse properties panel";
370
407
  readonly expandLeft: "Open blocks panel";
371
408
  readonly expandRight: "Open properties panel";
372
- readonly toggleSidebars: "Sidebar visibility";
373
- readonly toggleLeftSidebar: "Toggle left sidebar";
374
- readonly toggleRightSidebar: "Toggle right sidebar";
409
+ readonly selectProducts: "Select from Shopify";
410
+ readonly selectingProducts: "Selecting products…";
411
+ readonly selectProductsFailed: "Could not select products. Try again.";
412
+ readonly emptyProducts: "No products selected";
413
+ readonly productCountSuffix: "products";
414
+ readonly selectAsset: "Select image";
415
+ readonly selectingAsset: "Uploading image…";
416
+ readonly selectAssetFailed: "Could not upload the image. Try again.";
417
+ readonly emptyAsset: "No image selected";
418
+ readonly removeAsset: "Remove image";
375
419
  };
376
420
  };
377
421
  type AdminMessageKey = keyof typeof messages["zh-CN"];
378
422
  /** Minimal, dependency-free Admin copy catalog. Product locale remains PageDocument.settings.locale. */
379
423
  declare function createAdminI18n(locale?: string): {
380
424
  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";
425
+ 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" | "Select image" | "Uploading image…" | "Could not upload the image. Try again." | "No image selected" | "Remove image";
382
426
  };
383
427
 
384
428
  declare function toEngineData(document: PageDocument, registry?: ExtensionRegistry): Data;
385
429
  declare function fromEngineData(data: Data, base: PageDocument, registry?: ExtensionRegistry): PageDocument;
386
430
 
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 };
431
+ 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-RLKWR5JM.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,16 @@ 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",
826
+ selectAsset: "\u9009\u62E9\u56FE\u7247",
827
+ selectingAsset: "\u6B63\u5728\u4E0A\u4F20\u56FE\u7247\u2026",
828
+ selectAssetFailed: "\u65E0\u6CD5\u4E0A\u4F20\u56FE\u7247\uFF0C\u8BF7\u91CD\u8BD5\u3002",
829
+ emptyAsset: "\u672A\u8BBE\u7F6E\u56FE\u7247",
830
+ removeAsset: "\u79FB\u9664\u56FE\u7247"
820
831
  },
821
832
  en: {
822
833
  blocks: "Blocks",
@@ -841,6 +852,7 @@ var messages = {
841
852
  unsaved: "Unsaved changes",
842
853
  saved: "Changes are kept in this session",
843
854
  save: "Save draft",
855
+ history: "History",
844
856
  saving: "Saving\u2026",
845
857
  publish: "Publish",
846
858
  publishing: "Publishing\u2026",
@@ -869,9 +881,16 @@ var messages = {
869
881
  collapseRight: "Collapse properties panel",
870
882
  expandLeft: "Open blocks panel",
871
883
  expandRight: "Open properties panel",
872
- toggleSidebars: "Sidebar visibility",
873
- toggleLeftSidebar: "Toggle left sidebar",
874
- toggleRightSidebar: "Toggle right sidebar"
884
+ selectProducts: "Select from Shopify",
885
+ selectingProducts: "Selecting products\u2026",
886
+ selectProductsFailed: "Could not select products. Try again.",
887
+ emptyProducts: "No products selected",
888
+ productCountSuffix: "products",
889
+ selectAsset: "Select image",
890
+ selectingAsset: "Uploading image\u2026",
891
+ selectAssetFailed: "Could not upload the image. Try again.",
892
+ emptyAsset: "No image selected",
893
+ removeAsset: "Remove image"
875
894
  }
876
895
  };
877
896
  function createAdminI18n(locale) {
@@ -899,12 +918,6 @@ function blockTypeLabel(type, registry) {
899
918
  if (type === "core.image") return "\u56FE\u7247";
900
919
  return registry?.getBlock(type)?.label ?? type;
901
920
  }
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
921
  function validateDocumentBlocks(document, registry) {
909
922
  return registry ? validatePageDocumentWithRegistry(document, registry) : [];
910
923
  }
@@ -957,7 +970,7 @@ function PageDocumentEditorShell(props) {
957
970
  }, [managedSession.state, onSessionStateChange]);
958
971
  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
972
  }
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 }) {
973
+ function PageDocumentEditor({ iframe = false, registry, adminLocale, onSave, onPublish, draftPersistence, draftRevision: initialDraftRevision, autoSave = true, autoSaveDelayMs = 800, publishAction, assetPicker, inspectorSettings, productPicker, pageStatus, onBack, onHistory, onPreview, onAddToStore, deleteConfirmation, availableBlockTypes, appearanceControls = false }) {
961
974
  const editor = useEditorContext();
962
975
  const [blockView, setBlockView] = useState3("blocks");
963
976
  const [leftRailOpen, setLeftRailOpen] = useState3(true);
@@ -1104,25 +1117,24 @@ function PageDocumentEditor({ iframe = false, registry, adminLocale, onSave, onP
1104
1117
  editor.document.target
1105
1118
  ] }),
1106
1119
  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
1120
  ] })
1112
1121
  ] }),
1113
- /* @__PURE__ */ jsxs5("div", { className: "pb-header-device-toolbar", children: [
1114
- /* @__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
- /* @__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: [
1118
- /* @__PURE__ */ jsx7(Badge3, { tone: saveBadgeTone, children: saveBadgeLabel }),
1119
- /* @__PURE__ */ jsx7(Button2, { disabled: !canPersistDraft || !editor.isDirty || request !== "idle" || validationIssues.length > 0 || !isOnline, onClick: () => void save(), children: saveButtonLabel }),
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
- onAddToStore ? /* @__PURE__ */ jsx7(Button2, { disabled: request !== "idle" || pageStatus?.publicationStatus === "unpublished", onClick: () => void onAddToStore({ document: editor.document, session: editor.session }), children: i18n.t("addToStore") }) : null,
1122
- /* @__PURE__ */ jsx7(Button2, { variant: "primary", disabled: !publishAction && !onPublish || request !== "idle" || validationIssues.length > 0 || !isOnline, onClick: () => void publish(), children: request === "publishing" ? i18n.t("publishing") : i18n.t("publish") }),
1123
- /* @__PURE__ */ jsx7(Button2, { accessibilityLabel: i18n.t("undo"), icon: UndoIcon2, variant: "tertiary", disabled: !editor.actionState.canUndo, onClick: editor.undo }),
1124
- /* @__PURE__ */ jsx7(Button2, { accessibilityLabel: i18n.t("redo"), icon: RedoIcon2, variant: "tertiary", disabled: !editor.actionState.canRedo, onClick: editor.redo })
1125
- ] }) })
1122
+ /* @__PURE__ */ jsxs5("div", { className: "pb-header-controls", children: [
1123
+ /* @__PURE__ */ jsxs5("div", { className: "pb-header-device-toolbar", children: [
1124
+ /* @__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)) }),
1125
+ /* @__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) }) })
1126
+ ] }),
1127
+ /* @__PURE__ */ jsx7("div", { className: "pb-header-actions", children: /* @__PURE__ */ jsxs5(InlineStack3, { gap: "150", blockAlign: "center", wrap: true, children: [
1128
+ /* @__PURE__ */ jsx7(Badge3, { tone: saveBadgeTone, children: saveBadgeLabel }),
1129
+ 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,
1130
+ /* @__PURE__ */ jsx7(Button2, { disabled: !canPersistDraft || !editor.isDirty || request !== "idle" || validationIssues.length > 0 || !isOnline, onClick: () => void save(), children: saveButtonLabel }),
1131
+ onPreview ? /* @__PURE__ */ jsx7(Button2, { disabled: request !== "idle" || !isOnline, onClick: () => void onPreview({ document: editor.document, draftRevision, session: editor.session }), children: i18n.t("preview") }) : null,
1132
+ onAddToStore ? /* @__PURE__ */ jsx7(Button2, { disabled: request !== "idle" || pageStatus?.publicationStatus === "unpublished", onClick: () => void onAddToStore({ document: editor.document, session: editor.session }), children: i18n.t("addToStore") }) : null,
1133
+ /* @__PURE__ */ jsx7(Button2, { variant: "primary", disabled: !publishAction && !onPublish || request !== "idle" || validationIssues.length > 0 || !isOnline, onClick: () => void publish(), children: request === "publishing" ? i18n.t("publishing") : i18n.t("publish") }),
1134
+ /* @__PURE__ */ jsx7(Button2, { accessibilityLabel: i18n.t("undo"), icon: UndoIcon2, variant: "tertiary", disabled: !editor.actionState.canUndo, onClick: editor.undo }),
1135
+ /* @__PURE__ */ jsx7(Button2, { accessibilityLabel: i18n.t("redo"), icon: RedoIcon2, variant: "tertiary", disabled: !editor.actionState.canRedo, onClick: editor.redo })
1136
+ ] }) })
1137
+ ] })
1126
1138
  ] }) }),
1127
1139
  editor.loadState === "success" ? /* @__PURE__ */ jsx7(Banner, { tone: "success", children: i18n.t("success") }) : null,
1128
1140
  notice ? /* @__PURE__ */ jsx7(Banner, { tone: notice === "published" ? "success" : "critical", children: i18n.t(notice) }) : null,
@@ -1189,7 +1201,7 @@ function PageDocumentEditor({ iframe = false, registry, adminLocale, onSave, onP
1189
1201
  /* @__PURE__ */ jsx7(Button2, { accessibilityLabel: i18n.t("collapseRight"), icon: XIcon2, variant: "tertiary", onClick: () => setRightPanelOpen(false) })
1190
1202
  ] }),
1191
1203
  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) }),
1204
+ /* @__PURE__ */ jsx7(DocumentInspector, { settings: inspectorSettings?.[editor.selectedBlock.id] ?? inspectorSettings?.[editor.selectedBlock.type], 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) }, editor.selectedBlock.id),
1193
1205
  /* @__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
1206
  ] }) : /* @__PURE__ */ jsx7(Text3, { as: "p", tone: "subdued", children: i18n.t("selectBlock") })
1195
1207
  ] })
@@ -1260,7 +1272,66 @@ function InspectorTextControl({ label, value, control, disabled, onChange }) {
1260
1272
  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
1273
  return /* @__PURE__ */ jsx7(TextField, { label, labelHidden: true, value: stringValue, onChange, autoComplete: "off", disabled, multiline: control === "textarea" ? 4 : false, type: control === "url" ? "url" : "text" });
1262
1274
  }
1263
- function InspectorField({ name, field, value, registry, disabled, onChange }) {
1275
+ function InspectorAssetControl({ value, disabled, picker, pageId, blockId, i18n, onChange }) {
1276
+ const [busy, setBusy] = useState3(false);
1277
+ const [error, setError] = useState3(null);
1278
+ const active = useRef4(true);
1279
+ useEffect3(() => {
1280
+ active.current = !disabled;
1281
+ return () => {
1282
+ active.current = false;
1283
+ };
1284
+ }, [disabled]);
1285
+ const url = typeof value === "string" && validateFieldValue({ field: "", control: "asset" }, value).length === 0 ? value : "";
1286
+ return /* @__PURE__ */ jsxs5("div", { className: "pb-asset-picker", "data-testid": "asset-picker", children: [
1287
+ url ? /* @__PURE__ */ jsx7("img", { src: url, alt: "", className: "pb-asset-picker__preview" }) : /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "subdued", children: i18n.t("emptyAsset") }),
1288
+ url ? /* @__PURE__ */ jsx7(Button2, { fullWidth: true, variant: "tertiary", disabled: disabled || busy, onClick: () => onChange(""), children: i18n.t("removeAsset") }) : null,
1289
+ /* @__PURE__ */ jsx7(Button2, { fullWidth: true, disabled: disabled || busy || !picker, onClick: () => void (async () => {
1290
+ if (!picker) return;
1291
+ setBusy(true);
1292
+ setError(null);
1293
+ try {
1294
+ const asset = await picker.selectAsset({ pageId, blockId, current: { url } });
1295
+ if (asset && active.current) {
1296
+ if (validateFieldValue({ field: "", control: "asset", required: true }, asset.url).length) throw new Error("Invalid image URL");
1297
+ onChange(asset.url);
1298
+ }
1299
+ } catch {
1300
+ setError(i18n.t("selectAssetFailed"));
1301
+ } finally {
1302
+ setBusy(false);
1303
+ }
1304
+ })(), children: busy ? i18n.t("selectingAsset") : i18n.t("selectAsset") }),
1305
+ error ? /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "critical", children: error }) : null
1306
+ ] });
1307
+ }
1308
+ function InspectorProductsControl({ value, disabled, picker, pageId, blockId, i18n, onChange }) {
1309
+ const products = parseProductReferences(value);
1310
+ const [busy, setBusy] = useState3(false);
1311
+ const [error, setError] = useState3(null);
1312
+ const preview = products.slice(0, 4);
1313
+ return /* @__PURE__ */ jsxs5("div", { className: "pb-product-picker", "data-testid": "product-picker", children: [
1314
+ /* @__PURE__ */ jsxs5("div", { className: "pb-product-picker__summary", children: [
1315
+ 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,
1316
+ /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", children: products.length ? `${products.length} ${i18n.t("productCountSuffix")}` : i18n.t("emptyProducts") })
1317
+ ] }),
1318
+ /* @__PURE__ */ jsx7(Button2, { fullWidth: true, icon: ProductIcon, variant: "secondary", disabled: disabled || busy || !picker, onClick: () => void (async () => {
1319
+ if (!picker) return;
1320
+ setBusy(true);
1321
+ setError(null);
1322
+ try {
1323
+ const next = await picker.selectProducts({ pageId, blockId, current: products, multiple: true });
1324
+ if (next) onChange(parseProductReferences(next).map(toProductReferenceJson));
1325
+ } catch {
1326
+ setError(i18n.t("selectProductsFailed"));
1327
+ } finally {
1328
+ setBusy(false);
1329
+ }
1330
+ })(), children: busy ? i18n.t("selectingProducts") : i18n.t("selectProducts") }),
1331
+ error ? /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "critical", children: error }) : null
1332
+ ] });
1333
+ }
1334
+ function InspectorField({ name, field, value, registry, disabled, assetPicker, productPicker, pageId, blockId, i18n, onChange }) {
1264
1335
  const label = field.label ?? name;
1265
1336
  const issues = validateFieldValue(field, value);
1266
1337
  const Field = registry?.getField(field.field)?.component;
@@ -1269,7 +1340,7 @@ function InspectorField({ name, field, value, registry, disabled, onChange }) {
1269
1340
  /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", fontWeight: "semibold", children: label }),
1270
1341
  field.description ? /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "subdued", children: field.description }) : null
1271
1342
  ] }),
1272
- field.control ? /* @__PURE__ */ jsx7(InspectorTextControl, { label, value, control: field.control, disabled, onChange: (next) => onChange(next) }) : Field ? /* @__PURE__ */ jsx7(Field, { value, onChange }) : null,
1343
+ field.control === "products" ? /* @__PURE__ */ jsx7(InspectorProductsControl, { value, disabled, picker: productPicker, pageId, blockId, i18n, onChange }) : field.control === "asset" ? /* @__PURE__ */ jsx7(InspectorAssetControl, { value, disabled, picker: assetPicker, 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
1344
  issues.map((issue) => /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "critical", children: issue.message }, issue.message))
1274
1345
  ] });
1275
1346
  }
@@ -1308,7 +1379,7 @@ function inspectorFieldConfig(name, field) {
1308
1379
  return { ...field, group: field.group ?? group, description };
1309
1380
  }
1310
1381
  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 }) {
1382
+ function DocumentInspector({ settings, pageId, assetPicker, productPicker, i18n, block, registry, disabled, appearanceControls, onChange, onPresentationChange }) {
1312
1383
  const [assetError, setAssetError] = useState3(null);
1313
1384
  const [selectingAsset, setSelectingAsset] = useState3(false);
1314
1385
  const definition = registry?.getBlock(block.type);
@@ -1327,10 +1398,10 @@ function DocumentInspector({ pageId, assetPicker, block, registry, disabled, app
1327
1398
  /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "subdued", children: definition?.category ?? "Core block" })
1328
1399
  ] })
1329
1400
  ] }),
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,
1401
+ 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
1402
  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 }) }),
1403
+ /* @__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 }) }),
1404
+ /* @__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
1405
  assetPicker ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1335
1406
  /* @__PURE__ */ jsx7(Button2, { disabled: disabled || selectingAsset, onClick: () => void (async () => {
1336
1407
  setSelectingAsset(true);
@@ -1347,7 +1418,8 @@ function DocumentInspector({ pageId, assetPicker, block, registry, disabled, app
1347
1418
  assetError ? /* @__PURE__ */ jsx7(Text3, { as: "p", variant: "bodySm", tone: "critical", children: assetError }) : null
1348
1419
  ] }) : null
1349
1420
  ] }) : 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)),
1421
+ 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: field.persist === false ? settings?.values[name] : block.props[name], registry, disabled: disabled || field.persist === false && (!settings || Boolean(settings.disabled)), assetPicker, productPicker, pageId, blockId: block.id, i18n, onChange: (value) => field.persist === false ? settings?.onChange(name, value) : onChange({ [name]: value }) }, name)) }, group)),
1422
+ Object.values(definition?.fields ?? {}).some((field) => field.persist === false) ? settings?.footer : null,
1351
1423
  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
1424
  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
1425
  const style = { ...block.style };
@@ -1371,14 +1443,17 @@ export {
1371
1443
  createPageDocument,
1372
1444
  createTemplateRegistry,
1373
1445
  fromEngineData,
1446
+ isProductReference,
1374
1447
  maximumBlockInstances,
1375
1448
  mergeBlockPolicies,
1376
1449
  mergeThemeTokens,
1377
1450
  migratePageDocument,
1378
1451
  minimumBlockInstances,
1379
1452
  normalizeThemeTokens,
1453
+ parseProductReferences,
1380
1454
  systemThemeTokens,
1381
1455
  toEngineData,
1456
+ toProductReferenceJson,
1382
1457
  toThemeStyle,
1383
1458
  useEditorContext,
1384
1459
  validateBlockPolicy,
@@ -42,9 +42,11 @@ 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" | "asset";
46
46
  description?: string;
47
47
  group?: string;
48
+ /** Host-owned setting; never written to PageDocument props. */
49
+ persist?: boolean;
48
50
  /** Kept for compatibility; it participates in the built-in validation. */
49
51
  required?: boolean;
50
52
  validation?: FieldValidation;
@@ -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-BkqtMP-A.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-BkqtMP-A.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-RLKWR5JM.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.1",
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 12rem; 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: 2 1 28rem; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--builder-space-2) var(--builder-space-3); min-width: 0; }
17
+ .pb-header-device-toolbar { display: flex; flex: 0 1 auto; align-items: center; gap: var(--builder-space-2); min-width: 0; }
18
+ .pb-header-actions { display: flex; flex: 1 1 16rem; 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,16 @@ 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; }
63
+ .pb-asset-picker { display: grid; gap: 8px; }
64
+ .pb-asset-picker__preview { display: block; width: 100%; max-height: 180px; object-fit: cover; border: 1px solid #e2e8f0; border-radius: 10px; background: #f8fafc; }
53
65
  .pb-inspector__hint { margin: 0; padding: 0 var(--builder-space-2); color: #64748b; font-size: 12px; line-height: 1.5; }
54
66
  .pb-inspector-actions { padding-top: var(--builder-space-2); border-top: 1px solid #e2e8f0; }
55
67
  .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 +110,8 @@ button:focus-visible, [role="button"]:focus-visible { outline: 2px solid var(--b
98
110
  .pb-library-block-drag-hint svg { width: 16px; height: 16px; }
99
111
  .pb-document-block-row:hover { background: var(--builder-surface-subtle); }
100
112
  .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); }
113
+ .pb-editor-status { display: grid; place-items: center; box-sizing: border-box; width: 100%; min-height: 100dvh; padding: var(--builder-space-6); }
114
+ .pb-editor-status > * { width: min(100%, 28rem); justify-self: center; }
102
115
  .pb-shell--v04 > .Polaris-Banner { margin: var(--builder-space-3) var(--builder-space-4) 0; }
103
116
  .pb-document-canvas__text, .pb-document-canvas__image { max-width: 760px; margin: 0 auto; padding: 48px; color: #1d2939; background: #fff; cursor: pointer; }
104
117
  .pb-document-canvas__text { font-size: clamp(32px, 4vw, 52px); font-weight: 650; line-height: 1.1; letter-spacing: -.03em; }
@@ -131,5 +144,5 @@ button:focus-visible, [role="button"]:focus-visible { outline: 2px solid var(--b
131
144
  .bt-storefront__form input { flex: 1; min-width: 0; padding: 13px 14px; border: 1px solid #b8c3cc; border-radius: 8px; font: inherit; }
132
145
  .bt-storefront__form button { padding: 13px 18px; border: 0; border-radius: 8px; color: white; background: #203e54; font: inherit; font-weight: 600; }
133
146
  @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; } }
147
+ @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; } }
148
+ @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-basis: 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; } }