@standhigher/puck-page-builder 0.1.0 → 0.8.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.
@@ -0,0 +1,25 @@
1
+ import {
2
+ mergeThemeTokens,
3
+ toThemeStyle
4
+ } from "./chunk-YK2AUHF6.js";
5
+
6
+ // src/renderer/web/WebRenderer.tsx
7
+ import { jsx } from "react/jsx-runtime";
8
+ function WebRenderer({ document, className, registry }) {
9
+ const template = document.templateId ? registry?.getTemplate(document.templateId) : void 0;
10
+ const pageTheme = mergeThemeTokens(template?.theme, document.theme);
11
+ return /* @__PURE__ */ jsx("main", { className: className ?? "pb-web-renderer", "data-page-id": document.pageId, lang: document.settings.locale, style: toThemeStyle(pageTheme), children: document.blocks.map((block) => {
12
+ const definition = registry?.getBlock(block.type);
13
+ const variant = definition?.variants?.find((item) => item.id === block.variant);
14
+ const style = toThemeStyle(mergeThemeTokens(template?.theme, document.theme, variant?.theme, block.style));
15
+ if (block.type === "core.text") return /* @__PURE__ */ jsx("section", { "data-block-id": block.id, "data-block-variant": block.variant, style, className: "pb-web-renderer__text", children: /* @__PURE__ */ jsx("p", { children: typeof block.props.content === "string" ? block.props.content : "" }) }, block.id);
16
+ if (block.type === "core.image") return /* @__PURE__ */ jsx("figure", { "data-block-id": block.id, "data-block-variant": block.variant, style, className: "pb-web-renderer__image", children: /* @__PURE__ */ jsx("img", { src: typeof block.props.src === "string" ? block.props.src : "", alt: typeof block.props.alt === "string" ? block.props.alt : "" }) }, block.id);
17
+ const BlockRenderer = definition?.render.web;
18
+ if (BlockRenderer) return /* @__PURE__ */ jsx("section", { "data-block-id": block.id, "data-block-variant": block.variant, style, children: /* @__PURE__ */ jsx(BlockRenderer, { ...block.props }) }, block.id);
19
+ return null;
20
+ }) });
21
+ }
22
+
23
+ export {
24
+ WebRenderer
25
+ };
@@ -1,6 +1,10 @@
1
+ import {
2
+ normalizeThemeTokens
3
+ } from "./chunk-YK2AUHF6.js";
4
+
1
5
  // src/core/schema/page-document.ts
2
- var pageDocumentKeys = /* @__PURE__ */ new Set(["schemaVersion", "pageId", "target", "templateId", "root", "blocks", "settings"]);
3
- var blockKeys = /* @__PURE__ */ new Set(["id", "type", "version", "props", "slots", "binding"]);
6
+ var pageDocumentKeys = /* @__PURE__ */ new Set(["schemaVersion", "pageId", "target", "templateId", "templateVersion", "theme", "root", "blocks", "settings"]);
7
+ var blockKeys = /* @__PURE__ */ new Set(["id", "type", "version", "props", "variant", "style", "slots", "binding"]);
4
8
  function isRecord(value) {
5
9
  return typeof value === "object" && value !== null && !Array.isArray(value);
6
10
  }
@@ -9,6 +13,10 @@ function isJsonValue(value) {
9
13
  if (Array.isArray(value)) return value.every(isJsonValue);
10
14
  return isRecord(value) && Object.values(value).every(isJsonValue);
11
15
  }
16
+ function themeTokensOrEmpty(value) {
17
+ const result = normalizeThemeTokens(value);
18
+ return result.success ? result.data : {};
19
+ }
12
20
  function normalizeBlock(value, path, issues) {
13
21
  if (!isRecord(value)) {
14
22
  issues.push({ path, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
@@ -19,6 +27,8 @@ function normalizeBlock(value, path, issues) {
19
27
  if (typeof value.type !== "string" || !value.type) issues.push({ path: `${path}.type`, message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" });
20
28
  if (typeof value.version !== "number" || !Number.isInteger(value.version) || value.version < 1) issues.push({ path: `${path}.version`, message: "\u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u6574\u6570" });
21
29
  if (!isRecord(value.props) || !Object.values(value.props).every(isJsonValue)) issues.push({ path: `${path}.props`, message: "\u5FC5\u987B\u662F JSON \u5BF9\u8C61" });
30
+ if (typeof value.variant !== "string" || !value.variant) issues.push({ path: `${path}.variant`, message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" });
31
+ if (!normalizeThemeTokens(value.style).success) issues.push({ path: `${path}.style`, message: "\u5FC5\u987B\u662F\u53D7\u63A7 Theme Token \u5BF9\u8C61" });
22
32
  if (value.binding !== void 0 && (!isRecord(value.binding) || typeof value.binding.source !== "string" || value.binding.params !== void 0 && (!isRecord(value.binding.params) || !Object.values(value.binding.params).every(isJsonValue)))) issues.push({ path: `${path}.binding`, message: "\u5FC5\u987B\u5305\u542B source\uFF0C\u4E14 params \u5FC5\u987B\u662F JSON \u5BF9\u8C61" });
23
33
  if (issues.some((issue) => issue.path.startsWith(path))) return null;
24
34
  const slots = normalizeSlots(value.slots, `${path}.slots`, issues);
@@ -28,6 +38,8 @@ function normalizeBlock(value, path, issues) {
28
38
  type: value.type,
29
39
  version: value.version,
30
40
  props: value.props,
41
+ variant: value.variant,
42
+ style: themeTokensOrEmpty(value.style),
31
43
  ...slots ? { slots } : {},
32
44
  ...value.binding ? { binding: value.binding } : {}
33
45
  };
@@ -53,9 +65,10 @@ function createPageDocument(input) {
53
65
  schemaVersion: 1,
54
66
  pageId: input.pageId,
55
67
  target: input.target ?? "web",
56
- ...input.templateId ? { templateId: input.templateId } : {},
68
+ ...input.templateId ? { templateId: input.templateId, templateVersion: input.templateVersion ?? 1 } : {},
69
+ theme: themeTokensOrEmpty(input.theme),
57
70
  root: input.root ?? {},
58
- blocks: input.blocks ?? [],
71
+ blocks: (input.blocks ?? []).map((block) => ({ ...block, variant: block.variant ?? "default", style: themeTokensOrEmpty(block.style) })),
59
72
  settings: { locale: input.settings?.locale ?? "en", ...input.settings?.seoTitle ? { seoTitle: input.settings.seoTitle } : {} }
60
73
  };
61
74
  }
@@ -67,6 +80,10 @@ function validatePageDocument(value) {
67
80
  if (typeof value.pageId !== "string" || !value.pageId) issues.push({ path: "$.pageId", message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" });
68
81
  if (value.target !== "web" && value.target !== "email") issues.push({ path: "$.target", message: "\u5FC5\u987B\u662F web \u6216 email" });
69
82
  if (value.templateId !== void 0 && typeof value.templateId !== "string") issues.push({ path: "$.templateId", message: "\u5FC5\u987B\u662F\u5B57\u7B26\u4E32" });
83
+ if (value.templateVersion !== void 0 && (typeof value.templateVersion !== "number" || !Number.isInteger(value.templateVersion) || value.templateVersion < 1)) issues.push({ path: "$.templateVersion", message: "\u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u6574\u6570" });
84
+ if (typeof value.templateId === "string" && value.templateVersion === void 0) issues.push({ path: "$.templateVersion", message: "\u4F7F\u7528\u6A21\u677F\u65F6\u5FC5\u987B\u4FDD\u5B58\u6A21\u677F\u7248\u672C" });
85
+ if (value.templateId === void 0 && value.templateVersion !== void 0) issues.push({ path: "$.templateVersion", message: "\u672A\u4F7F\u7528\u6A21\u677F\u65F6\u4E0D\u5F97\u4FDD\u5B58\u6A21\u677F\u7248\u672C" });
86
+ if (!normalizeThemeTokens(value.theme).success) issues.push({ path: "$.theme", message: "\u5FC5\u987B\u662F\u53D7\u63A7 Theme Token \u5BF9\u8C61" });
70
87
  if (!isRecord(value.root) || !Object.values(value.root).every(isJsonValue)) issues.push({ path: "$.root", message: "\u5FC5\u987B\u662F JSON \u5BF9\u8C61" });
71
88
  if (!isRecord(value.settings) || typeof value.settings.locale !== "string") issues.push({ path: "$.settings.locale", message: "\u5FC5\u987B\u662F\u5B57\u7B26\u4E32" });
72
89
  if (!Array.isArray(value.blocks)) issues.push({ path: "$.blocks", message: "\u5FC5\u987B\u662F\u6570\u7EC4" });
@@ -83,6 +100,8 @@ function validatePageDocument(value) {
83
100
  pageId: value.pageId,
84
101
  target: value.target,
85
102
  ...typeof value.templateId === "string" ? { templateId: value.templateId } : {},
103
+ ...typeof value.templateVersion === "number" ? { templateVersion: value.templateVersion } : {},
104
+ theme: themeTokensOrEmpty(value.theme),
86
105
  root: value.root,
87
106
  blocks,
88
107
  settings: value.settings
@@ -90,14 +109,8 @@ function validatePageDocument(value) {
90
109
  };
91
110
  }
92
111
  function migratePageDocument(value) {
93
- if (!isRecord(value)) {
94
- const validation2 = validatePageDocument(value);
95
- return validation2.success ? { ...validation2, migrated: false } : validation2;
96
- }
97
- const migrated = value.schemaVersion === void 0;
98
- const candidate = migrated ? { ...value, schemaVersion: 1 } : value;
99
- const validation = validatePageDocument(candidate);
100
- return validation.success ? { ...validation, migrated } : validation;
112
+ const validation = validatePageDocument(value);
113
+ return validation.success ? { ...validation, migrated: false } : validation;
101
114
  }
102
115
 
103
116
  export {
@@ -56,8 +56,31 @@ function resolveOrder(extensions, disabled) {
56
56
  for (const extension of [...active].sort(compareByOrder)) visit(extension);
57
57
  return sorted;
58
58
  }
59
+ var TemplateRegistry = class _TemplateRegistry {
60
+ constructor(templateMap) {
61
+ this.templateMap = templateMap;
62
+ }
63
+ templateMap;
64
+ get templates() {
65
+ return Object.freeze([...this.templateMap.values()]);
66
+ }
67
+ get(id) {
68
+ return this.templateMap.get(id);
69
+ }
70
+ static create(templates, blocks) {
71
+ const blockTypes = new Set(blocks.map((block) => block.type));
72
+ for (const template of templates) {
73
+ if (template.source !== "built-in" && template.source !== "marketplace" && template.source !== "custom") throw new ExtensionRegistryError("invalid-identifier", `Template ${template.id} \u5FC5\u987B\u58F0\u660E built-in\u3001marketplace \u6216 custom \u6765\u6E90`);
74
+ if (template.target !== "web" && template.target !== "email") throw new ExtensionRegistryError("invalid-target", `Template ${template.id} \u5FC5\u987B\u58F0\u660E web \u6216 email target`);
75
+ for (const type of template.requiredBlocks ?? []) {
76
+ if (!blockTypes.has(type)) throw new ExtensionRegistryError("missing-template-dependency", `Template ${template.id} \u7F3A\u5C11\u5DF2\u6CE8\u518C\u533A\u5757\uFF1A${type}`);
77
+ }
78
+ }
79
+ return Object.freeze(new _TemplateRegistry(new Map(templates.map((template) => [template.id, template]))));
80
+ }
81
+ };
59
82
  var ExtensionRegistry = class _ExtensionRegistry {
60
- constructor(extensions, blockMap, fieldMap, actionMap, rendererMap, dataSourceMap, templateMap, slotMap, hooks) {
83
+ constructor(extensions, blockMap, fieldMap, actionMap, rendererMap, dataSourceMap, templateMap, templateRegistry, slotMap, hooks) {
61
84
  this.extensions = extensions;
62
85
  this.blockMap = blockMap;
63
86
  this.fieldMap = fieldMap;
@@ -65,6 +88,7 @@ var ExtensionRegistry = class _ExtensionRegistry {
65
88
  this.rendererMap = rendererMap;
66
89
  this.dataSourceMap = dataSourceMap;
67
90
  this.templateMap = templateMap;
91
+ this.templateRegistry = templateRegistry;
68
92
  this.slotMap = slotMap;
69
93
  this.hooks = hooks;
70
94
  }
@@ -75,6 +99,7 @@ var ExtensionRegistry = class _ExtensionRegistry {
75
99
  rendererMap;
76
100
  dataSourceMap;
77
101
  templateMap;
102
+ templateRegistry;
78
103
  slotMap;
79
104
  hooks;
80
105
  get blocks() {
@@ -93,7 +118,7 @@ var ExtensionRegistry = class _ExtensionRegistry {
93
118
  return Object.freeze([...this.dataSourceMap.values()]);
94
119
  }
95
120
  get templates() {
96
- return Object.freeze([...this.templateMap.values()]);
121
+ return this.templateRegistry.templates;
97
122
  }
98
123
  getBlock(type) {
99
124
  return this.blockMap.get(type);
@@ -111,7 +136,7 @@ var ExtensionRegistry = class _ExtensionRegistry {
111
136
  return this.dataSourceMap.get(key);
112
137
  }
113
138
  getTemplate(id) {
114
- return this.templateMap.get(id);
139
+ return this.templateRegistry.get(id);
115
140
  }
116
141
  getSlot(slot) {
117
142
  return this.slotMap.get(slot) ?? [];
@@ -154,6 +179,7 @@ var ExtensionRegistry = class _ExtensionRegistry {
154
179
  slots.set(slot.slot, current);
155
180
  }
156
181
  }
182
+ const templateRegistry = TemplateRegistry.create(templateItems.map((template) => templates.get(template.id)), blockItems);
157
183
  const sortedSlots = /* @__PURE__ */ new Map();
158
184
  for (const [slot, contributions] of slots) sortedSlots.set(slot, Object.freeze([...contributions].sort(compareByOrder)));
159
185
  return Object.freeze(new _ExtensionRegistry(
@@ -164,6 +190,7 @@ var ExtensionRegistry = class _ExtensionRegistry {
164
190
  renderers,
165
191
  dataSources,
166
192
  templates,
193
+ templateRegistry,
167
194
  sortedSlots,
168
195
  Object.freeze(resolved.flatMap((extension) => extension.hooks ? [extension.hooks] : []))
169
196
  ));
@@ -172,9 +199,15 @@ var ExtensionRegistry = class _ExtensionRegistry {
172
199
  function createExtensionRegistry(extensions, options) {
173
200
  return ExtensionRegistry.create(extensions, options);
174
201
  }
202
+ function createTemplateRegistry(templates, blocks = []) {
203
+ const registered = templates.map((template) => Object.freeze({ ...template, extension: "standalone" }));
204
+ return TemplateRegistry.create(registered, blocks);
205
+ }
175
206
 
176
207
  export {
177
208
  ExtensionRegistryError,
209
+ TemplateRegistry,
178
210
  ExtensionRegistry,
179
- createExtensionRegistry
211
+ createExtensionRegistry,
212
+ createTemplateRegistry
180
213
  };
@@ -0,0 +1,38 @@
1
+ // src/core/theme/index.ts
2
+ var systemThemeTokens = Object.freeze({
3
+ "color.background": "#ffffff",
4
+ "color.surface": "#ffffff",
5
+ "color.text": "#202223",
6
+ "color.muted": "#6d7175",
7
+ "color.primary": "#005bd3",
8
+ "color.border": "#d2d5d8",
9
+ "font.family": "system-ui, sans-serif",
10
+ "font.size": "16px",
11
+ radius: "8px",
12
+ spacing: "16px"
13
+ });
14
+ var themeTokenNames = new Set(Object.keys(systemThemeTokens));
15
+ var unsafeCssValue = /[{};]|url\s*\(|expression\s*\(|@import/i;
16
+ function normalizeThemeTokens(value) {
17
+ if (value === void 0) return { success: true, data: {} };
18
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return { success: false };
19
+ const tokens = {};
20
+ for (const [name, tokenValue] of Object.entries(value)) {
21
+ if (!themeTokenNames.has(name) || typeof tokenValue !== "string" || !tokenValue || unsafeCssValue.test(tokenValue)) return { success: false };
22
+ tokens[name] = tokenValue;
23
+ }
24
+ return { success: true, data: tokens };
25
+ }
26
+ function mergeThemeTokens(...layers) {
27
+ return Object.freeze(Object.assign({}, systemThemeTokens, ...layers));
28
+ }
29
+ function toThemeStyle(tokens) {
30
+ return Object.fromEntries(Object.entries(tokens).map(([name, value]) => [`--pb-${name.replaceAll(".", "-")}`, value]));
31
+ }
32
+
33
+ export {
34
+ systemThemeTokens,
35
+ normalizeThemeTokens,
36
+ mergeThemeTokens,
37
+ toThemeStyle
38
+ };
@@ -1,5 +1,6 @@
1
1
  import { ComponentType, ReactNode } from 'react';
2
- import { RenderTarget, PageDocument } from './schema.js';
2
+ import { RenderTarget, JsonValue, PageDocument } from './schema.js';
3
+ import { ThemeTokens } from './theme.js';
3
4
 
4
5
  type ExtensionTarget = RenderTarget | "all";
5
6
  type EditorActionPosition = "left" | "center" | "right";
@@ -11,12 +12,26 @@ type ValidationIssue = {
11
12
  type FieldConfig = {
12
13
  field: string;
13
14
  label?: string;
15
+ /** Product-facing editor metadata. Custom field components remain supported. */
16
+ control?: "text" | "textarea" | "url";
17
+ description?: string;
18
+ group?: string;
14
19
  required?: boolean;
15
20
  };
16
21
  type FieldProps<T = unknown> = {
17
22
  value: T;
18
23
  onChange(value: T): void;
19
24
  };
25
+ /**
26
+ * Props available only to an extension's editor-canvas renderer.
27
+ * They are separate from Web rendering so editor interaction cannot invoke a
28
+ * host Runtime or become persisted operational data.
29
+ */
30
+ type BlockEditorProps<P = Record<string, unknown>> = P & {
31
+ blockId: string;
32
+ selected: boolean;
33
+ onPropsChange(props: Record<string, JsonValue>): void;
34
+ };
20
35
  interface BlockDefinition<P = Record<string, unknown>> {
21
36
  type: string;
22
37
  version: number;
@@ -25,10 +40,20 @@ interface BlockDefinition<P = Record<string, unknown>> {
25
40
  targets: ExtensionTarget[];
26
41
  defaultProps: P;
27
42
  fields: Record<string, FieldConfig>;
28
- render: Partial<Record<RenderTarget, ComponentType<P>>>;
43
+ render: Partial<Record<RenderTarget, ComponentType<P>>> & {
44
+ /** Optional edit-mode renderer. WebRenderer never uses this surface. */
45
+ editor?: ComponentType<BlockEditorProps<P>>;
46
+ };
47
+ defaultVariant?: string;
48
+ variants?: BlockVariantDefinition[];
29
49
  dataSources?: string[];
30
50
  validate?: (props: P) => ValidationIssue[];
31
51
  }
52
+ interface BlockVariantDefinition {
53
+ id: string;
54
+ label: string;
55
+ theme?: ThemeTokens;
56
+ }
32
57
  interface FieldDefinition<T = unknown> {
33
58
  type: string;
34
59
  component: ComponentType<FieldProps<T>>;
@@ -65,13 +90,18 @@ interface DataSourceDefinition<P = any, R = unknown> {
65
90
  live: (params: P) => Promise<R>;
66
91
  validateParams?: (params: P) => ValidationIssue[];
67
92
  }
93
+ type TemplateSource = "built-in" | "marketplace" | "custom";
68
94
  interface TemplateDefinition {
69
95
  id: string;
70
96
  version: number;
71
97
  name: string;
72
98
  target: RenderTarget;
99
+ source: TemplateSource;
73
100
  thumbnail?: string;
74
101
  create(): PageDocument;
102
+ /** Block types that must be registered before this template can be used. */
103
+ requiredBlocks?: string[];
104
+ theme?: ThemeTokens;
75
105
  }
76
106
  interface LifecycleHooks {
77
107
  onChange?(document: PageDocument): void;
@@ -106,7 +136,7 @@ type ExtensionRegistryOptions = {
106
136
  disabled?: string[];
107
137
  };
108
138
 
109
- type ExtensionRegistryErrorCode = "duplicate-extension" | "duplicate-definition" | "invalid-identifier" | "invalid-target" | "missing-dependency" | "dependency-cycle";
139
+ type ExtensionRegistryErrorCode = "duplicate-extension" | "duplicate-definition" | "invalid-identifier" | "invalid-target" | "missing-dependency" | "missing-template-dependency" | "dependency-cycle";
110
140
  declare class ExtensionRegistryError extends Error {
111
141
  readonly code: ExtensionRegistryErrorCode;
112
142
  constructor(code: ExtensionRegistryErrorCode, message: string);
@@ -114,6 +144,14 @@ declare class ExtensionRegistryError extends Error {
114
144
  type Registered<T> = T & {
115
145
  extension: string;
116
146
  };
147
+ /** Immutable template view with explicit source and block dependency validation. */
148
+ declare class TemplateRegistry {
149
+ private readonly templateMap;
150
+ private constructor();
151
+ get templates(): readonly Registered<TemplateDefinition>[];
152
+ get(id: string): Registered<TemplateDefinition> | undefined;
153
+ static create(templates: readonly Registered<TemplateDefinition>[], blocks: readonly BlockDefinition[]): TemplateRegistry;
154
+ }
117
155
  /** Immutable compiled view of all enabled extensions. */
118
156
  declare class ExtensionRegistry {
119
157
  readonly extensions: readonly PageBuilderExtension[];
@@ -123,6 +161,7 @@ declare class ExtensionRegistry {
123
161
  private readonly rendererMap;
124
162
  private readonly dataSourceMap;
125
163
  private readonly templateMap;
164
+ readonly templateRegistry: TemplateRegistry;
126
165
  private readonly slotMap;
127
166
  private readonly hooks;
128
167
  private constructor();
@@ -144,5 +183,6 @@ declare class ExtensionRegistry {
144
183
  static create(extensions: PageBuilderExtension[], options?: ExtensionRegistryOptions): ExtensionRegistry;
145
184
  }
146
185
  declare function createExtensionRegistry(extensions: PageBuilderExtension[], options?: ExtensionRegistryOptions): ExtensionRegistry;
186
+ declare function createTemplateRegistry(templates: TemplateDefinition[], blocks?: BlockDefinition[]): TemplateRegistry;
147
187
 
148
- export { type BlockDefinition, type DataSourceDefinition, type EditorAction, type EditorActionPosition, type ExtensionActionContext, ExtensionRegistry, ExtensionRegistryError, type ExtensionRegistryErrorCode, type ExtensionRegistryOptions, type ExtensionTarget, type FieldConfig, type FieldDefinition, type FieldProps, type LifecycleHooks, type PageBuilderExtension, type RendererDefinition, type TemplateDefinition, type UISlotContribution, type UISlotName, type ValidationIssue, createExtensionRegistry };
188
+ export { type BlockDefinition, type BlockEditorProps, type BlockVariantDefinition, type DataSourceDefinition, type EditorAction, type EditorActionPosition, type ExtensionActionContext, ExtensionRegistry, ExtensionRegistryError, type ExtensionRegistryErrorCode, type ExtensionRegistryOptions, type ExtensionTarget, type FieldConfig, type FieldDefinition, type FieldProps, type LifecycleHooks, type PageBuilderExtension, type RendererDefinition, type TemplateDefinition, TemplateRegistry, type TemplateSource, type UISlotContribution, type UISlotName, type ValidationIssue, createExtensionRegistry, createTemplateRegistry };
@@ -1,10 +1,14 @@
1
1
  import {
2
2
  ExtensionRegistry,
3
3
  ExtensionRegistryError,
4
- createExtensionRegistry
5
- } from "./chunk-RGB53WE6.js";
4
+ TemplateRegistry,
5
+ createExtensionRegistry,
6
+ createTemplateRegistry
7
+ } from "./chunk-PUTYZI7B.js";
6
8
  export {
7
9
  ExtensionRegistry,
8
10
  ExtensionRegistryError,
9
- createExtensionRegistry
11
+ TemplateRegistry,
12
+ createExtensionRegistry,
13
+ createTemplateRegistry
10
14
  };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { ExtensionRegistry } from './extensions.js';
4
- export { BlockDefinition, DataSourceDefinition, EditorAction, EditorActionPosition, ExtensionActionContext, ExtensionRegistryError, ExtensionRegistryErrorCode, ExtensionRegistryOptions, ExtensionTarget, FieldConfig, FieldDefinition, FieldProps, LifecycleHooks, PageBuilderExtension, RendererDefinition, TemplateDefinition, UISlotContribution, UISlotName, ValidationIssue, createExtensionRegistry } from './extensions.js';
4
+ export { BlockDefinition, BlockEditorProps, BlockVariantDefinition, DataSourceDefinition, EditorAction, EditorActionPosition, ExtensionActionContext, ExtensionRegistryError, ExtensionRegistryErrorCode, ExtensionRegistryOptions, ExtensionTarget, FieldConfig, FieldDefinition, FieldProps, LifecycleHooks, PageBuilderExtension, RendererDefinition, TemplateDefinition, TemplateRegistry, TemplateSource, UISlotContribution, UISlotName, ValidationIssue, createExtensionRegistry, createTemplateRegistry } from './extensions.js';
5
5
  import { PageDocument, BlockNode, JsonValue } from './schema.js';
6
- export { DataBinding, PageDocumentIssue, PageDocumentMigration, PageSettings, RenderTarget, createPageDocument, migratePageDocument, validatePageDocument } from './schema.js';
6
+ export { BlockStyleOverrides, DataBinding, PageDocumentIssue, PageDocumentMigration, PageDocumentSchemaVersion, PageSettings, RenderTarget, createPageDocument, migratePageDocument, validatePageDocument } from './schema.js';
7
+ export { ThemeTokenName, ThemeTokens, mergeThemeTokens, normalizeThemeTokens, systemThemeTokens, toThemeStyle } from './theme.js';
7
8
  import { Data } from '@puckeditor/core';
8
9
  export { WebRenderer, WebRendererProps } from './renderer.js';
9
10
 
package/dist/index.js CHANGED
@@ -1,16 +1,24 @@
1
1
  import {
2
2
  ExtensionRegistry,
3
3
  ExtensionRegistryError,
4
- createExtensionRegistry
5
- } from "./chunk-RGB53WE6.js";
4
+ TemplateRegistry,
5
+ createExtensionRegistry,
6
+ createTemplateRegistry
7
+ } from "./chunk-PUTYZI7B.js";
6
8
  import {
7
9
  WebRenderer
8
- } from "./chunk-72MFV6J7.js";
10
+ } from "./chunk-KYXIXIWK.js";
9
11
  import {
10
12
  createPageDocument,
11
13
  migratePageDocument,
12
14
  validatePageDocument
13
- } from "./chunk-G2LNCKYO.js";
15
+ } from "./chunk-OBN4DMOR.js";
16
+ import {
17
+ mergeThemeTokens,
18
+ normalizeThemeTokens,
19
+ systemThemeTokens,
20
+ toThemeStyle
21
+ } from "./chunk-YK2AUHF6.js";
14
22
 
15
23
  // src/editor/shell/EditorShell.tsx
16
24
  import { Puck } from "@puckeditor/core";
@@ -334,12 +342,35 @@ function PropertyPanel({
334
342
 
335
343
  // src/editor/shell/PageDocumentEditorShell.tsx
336
344
  import { Puck as Puck2, usePuck } from "@puckeditor/core";
337
- import { Badge as Badge2, Banner, BlockStack as BlockStack2, Button as Button2, ButtonGroup as ButtonGroup2, InlineStack as InlineStack2, Text as Text2, TextField } from "@shopify/polaris";
345
+ import { Badge as Badge2, Banner, Button as Button2, ButtonGroup as ButtonGroup2, InlineStack as InlineStack2, Text as Text2, TextField } from "@shopify/polaris";
338
346
  import { DragHandleIcon as DragHandleIcon2, LayoutSectionIcon as LayoutSectionIcon2, MenuIcon as MenuIcon2, RedoIcon as RedoIcon2, UndoIcon as UndoIcon2 } from "@shopify/polaris-icons";
339
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef3, useState as useState3 } from "react";
347
+ import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef4, useState as useState3 } from "react";
348
+
349
+ // src/adapters/puck/canvas-extension-block.tsx
350
+ import { registerOverlayPortal } from "@puckeditor/core";
351
+ import { useEffect, useRef as useRef2 } from "react";
352
+ import { jsx as jsx3 } from "react/jsx-runtime";
353
+ function CanvasExtensionBlock({ active, children, label, onSelect }) {
354
+ const editorRef = useRef2(null);
355
+ useEffect(() => active ? registerOverlayPortal(editorRef.current, { disableDrag: true }) : void 0, [active]);
356
+ return /* @__PURE__ */ jsx3("div", { ref: editorRef, className: "pb-document-canvas__extension", "aria-label": "Select " + label + " in canvas", role: "group", tabIndex: 0, onClick: onSelect, onKeyDown: (event) => {
357
+ if (event.key === "Enter" || event.key === " ") onSelect();
358
+ }, children });
359
+ }
340
360
 
341
361
  // src/adapters/puck/page-document-config.tsx
342
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
362
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
363
+ function canvasFieldFallback({ block, props, registry, onPropsChange }) {
364
+ const fields = Object.entries(block.fields);
365
+ if (!fields.length) return null;
366
+ return /* @__PURE__ */ jsx4("div", { "aria-label": "Edit " + block.label + " values in canvas", onClick: (event) => event.stopPropagation(), style: { display: "grid", gap: 8, marginTop: 12, padding: 12, border: "1px dashed #94a3b8", borderRadius: 6, background: "#f8fafc" }, children: fields.map(([name, field]) => {
367
+ const Field = registry?.getField(field.field)?.component;
368
+ return Field ? /* @__PURE__ */ jsxs3("label", { style: { display: "grid", gap: 4, fontSize: 12, fontWeight: 600 }, children: [
369
+ field.label ?? name,
370
+ /* @__PURE__ */ jsx4(Field, { value: props[name], onChange: (value) => onPropsChange({ [name]: value }) })
371
+ ] }, name) : null;
372
+ }) });
373
+ }
343
374
  function createPageDocumentPuckConfig(onSelect, onPropsChange, selectedBlockId, registry) {
344
375
  const extensionComponents = Object.fromEntries((registry?.blocks ?? []).flatMap((block) => {
345
376
  const BlockRenderer = block.render.web;
@@ -347,9 +378,12 @@ function createPageDocumentPuckConfig(onSelect, onPropsChange, selectedBlockId,
347
378
  return [[block.type, {
348
379
  render: (props) => {
349
380
  const id = typeof props.id === "string" ? props.id : `unknown-${block.type}`;
350
- return /* @__PURE__ */ jsx3("section", { className: "pb-document-canvas__extension", "aria-label": `Select ${block.label} in canvas`, role: "button", tabIndex: 0, onClick: () => onSelect(id), onKeyDown: (event) => {
351
- if (event.key === "Enter" || event.key === " ") onSelect(id);
352
- }, children: /* @__PURE__ */ jsx3(BlockRenderer, { ...props }) });
381
+ const EditorRenderer = block.render.editor;
382
+ const active = id === selectedBlockId;
383
+ return /* @__PURE__ */ jsx4(CanvasExtensionBlock, { active, label: block.label, onSelect: () => onSelect(id), children: EditorRenderer ? /* @__PURE__ */ jsx4(EditorRenderer, { ...props, blockId: id, selected: active, onPropsChange: (next) => onPropsChange(id, next) }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
384
+ /* @__PURE__ */ jsx4(BlockRenderer, { ...props }),
385
+ active ? canvasFieldFallback({ block, props, registry, onPropsChange: (next) => onPropsChange(id, next) }) : null
386
+ ] }) });
353
387
  }
354
388
  }]];
355
389
  }));
@@ -360,10 +394,10 @@ function createPageDocumentPuckConfig(onSelect, onPropsChange, selectedBlockId,
360
394
  const id = typeof props.id === "string" ? props.id : "unknown-text";
361
395
  const content = typeof props.content === "string" ? props.content : "";
362
396
  const editable = id === selectedBlockId;
363
- return /* @__PURE__ */ jsx3("section", { className: "pb-document-canvas__text", "data-page-document-block-id": id, "aria-label": "Select Text in canvas", role: "button", tabIndex: 0, onClick: () => onSelect(id), onKeyDown: (event) => {
397
+ return /* @__PURE__ */ jsx4("section", { className: "pb-document-canvas__text", "data-page-document-block-id": id, "aria-label": "Select Text in canvas", role: "button", tabIndex: 0, onClick: () => onSelect(id), onKeyDown: (event) => {
364
398
  if (event.key === "Enter" || event.key === " ") onSelect(id);
365
- }, children: /* @__PURE__ */ jsx3("p", { contentEditable: editable, suppressContentEditableWarning: true, onInput: (event) => {
366
- if (editable) onPropsChange(id, { content: event.currentTarget.textContent ?? "" });
399
+ }, children: /* @__PURE__ */ jsx4("p", { contentEditable: editable, suppressContentEditableWarning: true, onInput: (event) => {
400
+ if (editable) onPropsChange(id, { content: event.currentTarget.textContent ?? "" }, true);
367
401
  }, children: content }) });
368
402
  }
369
403
  },
@@ -376,8 +410,8 @@ function createPageDocumentPuckConfig(onSelect, onPropsChange, selectedBlockId,
376
410
  return /* @__PURE__ */ jsxs3("figure", { className: "pb-document-canvas__image", "data-page-document-block-id": id, "aria-label": "Select Image in canvas", role: "button", tabIndex: 0, onClick: () => onSelect(id), onKeyDown: (event) => {
377
411
  if (event.key === "Enter" || event.key === " ") onSelect(id);
378
412
  }, children: [
379
- /* @__PURE__ */ jsx3("img", { src, alt }),
380
- editable ? /* @__PURE__ */ jsx3("input", { "aria-label": "\u753B\u5E03\u56FE\u7247 URL", value: src, onChange: (event) => onPropsChange(id, { src: event.currentTarget.value }), onClick: (event) => event.stopPropagation() }) : null
413
+ /* @__PURE__ */ jsx4("img", { src, alt }),
414
+ editable ? /* @__PURE__ */ jsx4("input", { "aria-label": "\u753B\u5E03\u56FE\u7247 URL", value: src, onChange: (event) => onPropsChange(id, { src: event.currentTarget.value }, true), onClick: (event) => event.stopPropagation() }) : null
381
415
  ] });
382
416
  }
383
417
  },
@@ -399,7 +433,7 @@ function toEngineBlock(block, registry) {
399
433
  }
400
434
  function toEngineData(document, registry) {
401
435
  return {
402
- root: { ...document.root },
436
+ root: { props: { ...document.root } },
403
437
  content: document.blocks.map((block) => toEngineBlock(block, registry))
404
438
  };
405
439
  }
@@ -416,6 +450,8 @@ function fromEngineData(data, base, registry) {
416
450
  type,
417
451
  version: previous?.version ?? 1,
418
452
  props,
453
+ variant: previous?.variant ?? registry?.getBlock(type)?.defaultVariant ?? "default",
454
+ style: previous?.style ?? {},
419
455
  ...previous?.slots ? { slots: previous.slots } : {},
420
456
  ...previous?.binding ? { binding: previous.binding } : {}
421
457
  }];
@@ -425,8 +461,8 @@ function fromEngineData(data, base, registry) {
425
461
  }
426
462
 
427
463
  // src/editor/context/EditorContext.tsx
428
- import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef as useRef2, useState as useState2 } from "react";
429
- import { jsx as jsx4 } from "react/jsx-runtime";
464
+ import { createContext, useCallback, useContext, useEffect as useEffect2, useMemo, useReducer, useRef as useRef3, useState as useState2 } from "react";
465
+ import { jsx as jsx5 } from "react/jsx-runtime";
430
466
  function historyReducer(state, action) {
431
467
  if (action.type === "select") return { ...state, selectedBlockId: action.id };
432
468
  if (action.type === "device") return { ...state, device: action.device };
@@ -452,16 +488,16 @@ function uniqueBlockId(type, blocks) {
452
488
  return `${prefix}-${index}`;
453
489
  }
454
490
  function defaultBlock(type, blocks, registry) {
455
- if (type === "core.text") return { id: uniqueBlockId(type, blocks), type, version: 1, props: { content: "New text block" } };
456
- if (type === "core.image") return { id: uniqueBlockId(type, blocks), type, version: 1, props: { src: "https://images.unsplash.com/photo-1580674684081-7617fbf3d745?auto=format&fit=crop&w=1200&q=80", alt: "" } };
491
+ if (type === "core.text") return { id: uniqueBlockId(type, blocks), type, version: 1, props: { content: "New text block" }, variant: "default", style: {} };
492
+ if (type === "core.image") return { id: uniqueBlockId(type, blocks), type, version: 1, props: { src: "https://images.unsplash.com/photo-1580674684081-7617fbf3d745?auto=format&fit=crop&w=1200&q=80", alt: "" }, variant: "default", style: {} };
457
493
  const definition = registry?.getBlock(type);
458
494
  if (!definition) throw new Error(`Unknown PageDocument block type: ${type}`);
459
- return { id: uniqueBlockId(type, blocks), type, version: definition.version, props: definition.defaultProps };
495
+ return { id: uniqueBlockId(type, blocks), type, version: definition.version, props: definition.defaultProps, variant: definition.defaultVariant ?? "default", style: {} };
460
496
  }
461
497
  function EditorProvider({ initialDocument, registry, loadState = "ready", leaveWarning = "You have unsaved changes.", onDocumentChange, children }) {
462
498
  const [history, dispatch] = useReducer(historyReducer, initialDocument, (document) => ({ document, selectedBlockId: document.blocks[0]?.id ?? null, past: [], future: [], device: "desktop", savedDocument: document }));
463
- const historyRef = useRef2(history);
464
- useEffect(() => {
499
+ const historyRef = useRef3(history);
500
+ useEffect2(() => {
465
501
  historyRef.current = history;
466
502
  }, [history]);
467
503
  const [canvasSelectionRequest, setCanvasSelectionRequest] = useState2(null);
@@ -486,11 +522,11 @@ function EditorProvider({ initialDocument, registry, loadState = "ready", leaveW
486
522
  const blocks = current.document.blocks.map((block) => block.id === id ? { ...block, props: { ...block.props, ...props } } : block);
487
523
  replace({ ...current.document, blocks }, id);
488
524
  }, [editable, replace]);
489
- useEffect(() => {
525
+ useEffect2(() => {
490
526
  onDocumentChange?.(history.document);
491
527
  }, [history.document, onDocumentChange]);
492
528
  const isDirty = JSON.stringify(history.document) !== JSON.stringify(history.savedDocument);
493
- useEffect(() => {
529
+ useEffect2(() => {
494
530
  if (!isDirty || typeof window === "undefined") return;
495
531
  const confirmLeave = (event) => {
496
532
  event.preventDefault();
@@ -499,7 +535,7 @@ function EditorProvider({ initialDocument, registry, loadState = "ready", leaveW
499
535
  window.addEventListener("beforeunload", confirmLeave);
500
536
  return () => window.removeEventListener("beforeunload", confirmLeave);
501
537
  }, [isDirty, leaveWarning]);
502
- useEffect(() => {
538
+ useEffect2(() => {
503
539
  if (typeof window === "undefined") return;
504
540
  const keydown = (event) => {
505
541
  const target = event.target;
@@ -594,7 +630,7 @@ function EditorProvider({ initialDocument, registry, loadState = "ready", leaveW
594
630
  markSaved: () => dispatch({ type: "saved" })
595
631
  };
596
632
  }, [canvasSelectionRequest, confirmCanvasSelection, editable, history, isDirty, loadState, registry, replace, requestCanvasSelection, selectedBlock, updateBlockProps, updateFromCanvas]);
597
- return /* @__PURE__ */ jsx4(EditorContext.Provider, { value, children });
633
+ return /* @__PURE__ */ jsx5(EditorContext.Provider, { value, children });
598
634
  }
599
635
  function useEditorContext() {
600
636
  const context = useContext(EditorContext);
@@ -682,7 +718,7 @@ function blockIdAtRelativeY(ids, relativeY) {
682
718
  }
683
719
 
684
720
  // src/editor/shell/PageDocumentEditorShell.tsx
685
- import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
721
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
686
722
  var deviceLabels2 = { desktop: "desktop", tablet: "tablet", mobile: "mobile" };
687
723
  function blockLabel(block, registry) {
688
724
  return blockTypeLabel(block.type, registry);
@@ -693,25 +729,25 @@ function blockTypeLabel(type, registry) {
693
729
  return registry?.getBlock(type)?.label ?? type;
694
730
  }
695
731
  function PageDocumentEditorShell(props) {
696
- return /* @__PURE__ */ jsx5(EditorProvider, { initialDocument: props.initialDocument, registry: props.registry, loadState: props.loadState, leaveWarning: createAdminI18n(props.adminLocale).t("leaveWarning"), onDocumentChange: props.onDocumentChange, children: /* @__PURE__ */ jsx5(PageDocumentEditor, { ...props }) });
732
+ return /* @__PURE__ */ jsx6(EditorProvider, { initialDocument: props.initialDocument, registry: props.registry, loadState: props.loadState, leaveWarning: createAdminI18n(props.adminLocale).t("leaveWarning"), onDocumentChange: props.onDocumentChange, children: /* @__PURE__ */ jsx6(PageDocumentEditor, { ...props }) });
697
733
  }
698
734
  function PageDocumentEditor({ iframe = true, registry, adminLocale, onSave, onPublish }) {
699
735
  const editor = useEditorContext();
700
736
  const [blockView, setBlockView] = useState3("blocks");
701
737
  const [draggingLibraryType, setDraggingLibraryType] = useState3(null);
702
- const canvasFrameRef = useRef3(null);
738
+ const canvasFrameRef = useRef4(null);
703
739
  const [canvasMutationVersion, setCanvasMutationVersion] = useState3(0);
704
740
  const [request, setRequest] = useState3("idle");
705
741
  const [notice, setNotice] = useState3(null);
706
742
  const i18n = createAdminI18n(adminLocale);
707
743
  const engineData = useMemo2(() => toEngineData(editor.document, registry), [editor.document, registry]);
708
744
  const { confirmCanvasSelection, selectedBlockId, updateBlockProps } = editor;
709
- const updateFromCanvasInput = useCallback2((id, props) => {
710
- setCanvasMutationVersion((version) => version + 1);
745
+ const updateFromCanvasInput = useCallback2((id, props, preserveCanvasValue = false) => {
746
+ if (preserveCanvasValue) setCanvasMutationVersion((version) => version + 1);
711
747
  updateBlockProps(id, props);
712
748
  }, [updateBlockProps]);
713
749
  const config = useMemo2(() => createPageDocumentPuckConfig(confirmCanvasSelection, updateFromCanvasInput, selectedBlockId, registry), [confirmCanvasSelection, registry, selectedBlockId, updateFromCanvasInput]);
714
- if (editor.loadState !== "ready" && editor.loadState !== "success") return /* @__PURE__ */ jsx5(EditorStatus, { state: editor.loadState });
750
+ if (editor.loadState !== "ready" && editor.loadState !== "success") return /* @__PURE__ */ jsx6(EditorStatus, { state: editor.loadState });
715
751
  const blockTypes = ["core.text", "core.image", ...registry?.blocks.map((block) => block.type) ?? []];
716
752
  const addFromLibrary = (type, beforeId) => {
717
753
  const id = editor.addBlock(type, beforeId);
@@ -775,12 +811,12 @@ function PageDocumentEditor({ iframe = true, registry, adminLocale, onSave, onPu
775
811
  setRequest("idle");
776
812
  }
777
813
  };
778
- return /* @__PURE__ */ jsx5(Puck2, { config, data: engineData, iframe: { enabled: iframe }, onChange: (data) => editor.updateFromCanvas(fromEngineData(data, editor.document, registry)), children: /* @__PURE__ */ jsxs4(Puck2.Layout, { children: [
779
- /* @__PURE__ */ jsx5(CanvasSelectionBridge, { data: engineData, requestedBlockId: editor.canvasSelectionRequest, onCanvasSelected: confirmCanvasSelection, canvasMutationVersion }),
814
+ return /* @__PURE__ */ jsx6(Puck2, { config, data: engineData, iframe: { enabled: iframe }, onChange: (data) => editor.updateFromCanvas(fromEngineData(data, editor.document, registry)), children: /* @__PURE__ */ jsxs4(Puck2.Layout, { children: [
815
+ /* @__PURE__ */ jsx6(CanvasSelectionBridge, { data: engineData, requestedBlockId: editor.canvasSelectionRequest, onCanvasSelected: confirmCanvasSelection, canvasMutationVersion }),
780
816
  /* @__PURE__ */ jsxs4("div", { className: "pb-shell pb-shell--v04", "data-testid": "page-document-editor", "data-page-id": editor.document.pageId, "data-dirty": editor.isDirty, "data-editor-state": editor.loadState, children: [
781
- /* @__PURE__ */ jsx5("header", { className: "pb-header", children: /* @__PURE__ */ jsxs4(InlineStack2, { align: "space-between", blockAlign: "center", gap: "300", wrap: false, children: [
817
+ /* @__PURE__ */ jsx6("header", { className: "pb-header", children: /* @__PURE__ */ jsxs4(InlineStack2, { align: "space-between", blockAlign: "center", gap: "300", wrap: false, children: [
782
818
  /* @__PURE__ */ jsxs4("div", { className: "pb-page-title", children: [
783
- /* @__PURE__ */ jsx5(Text2, { as: "h1", variant: "headingSm", children: editor.document.settings.seoTitle ?? editor.document.pageId }),
819
+ /* @__PURE__ */ jsx6(Text2, { as: "h1", variant: "headingSm", children: editor.document.settings.seoTitle ?? editor.document.pageId }),
784
820
  /* @__PURE__ */ jsxs4(Text2, { as: "p", variant: "bodySm", tone: "subdued", children: [
785
821
  "PageDocument V",
786
822
  editor.document.schemaVersion,
@@ -789,54 +825,54 @@ function PageDocumentEditor({ iframe = true, registry, adminLocale, onSave, onPu
789
825
  ] })
790
826
  ] }),
791
827
  /* @__PURE__ */ jsxs4(InlineStack2, { gap: "150", blockAlign: "center", wrap: false, children: [
792
- /* @__PURE__ */ jsx5(Badge2, { tone: editor.isDirty ? "attention" : "success", children: editor.isDirty ? i18n.t("unsaved") : i18n.t("saved") }),
793
- /* @__PURE__ */ jsx5(Button2, { disabled: !onSave || !editor.isDirty || request !== "idle", onClick: () => void save(), children: request === "saving" ? i18n.t("saving") : i18n.t("save") }),
794
- /* @__PURE__ */ jsx5(Button2, { variant: "primary", disabled: !onPublish || request !== "idle", onClick: () => void publish(), children: request === "publishing" ? i18n.t("publishing") : i18n.t("publish") }),
795
- /* @__PURE__ */ jsx5(Button2, { accessibilityLabel: i18n.t("undo"), icon: UndoIcon2, variant: "tertiary", disabled: !editor.actionState.canUndo, onClick: editor.undo }),
796
- /* @__PURE__ */ jsx5(Button2, { accessibilityLabel: i18n.t("redo"), icon: RedoIcon2, variant: "tertiary", disabled: !editor.actionState.canRedo, onClick: editor.redo })
828
+ /* @__PURE__ */ jsx6(Badge2, { tone: editor.isDirty ? "attention" : "success", children: editor.isDirty ? i18n.t("unsaved") : i18n.t("saved") }),
829
+ /* @__PURE__ */ jsx6(Button2, { disabled: !onSave || !editor.isDirty || request !== "idle", onClick: () => void save(), children: request === "saving" ? i18n.t("saving") : i18n.t("save") }),
830
+ /* @__PURE__ */ jsx6(Button2, { variant: "primary", disabled: !onPublish || request !== "idle", onClick: () => void publish(), children: request === "publishing" ? i18n.t("publishing") : i18n.t("publish") }),
831
+ /* @__PURE__ */ jsx6(Button2, { accessibilityLabel: i18n.t("undo"), icon: UndoIcon2, variant: "tertiary", disabled: !editor.actionState.canUndo, onClick: editor.undo }),
832
+ /* @__PURE__ */ jsx6(Button2, { accessibilityLabel: i18n.t("redo"), icon: RedoIcon2, variant: "tertiary", disabled: !editor.actionState.canRedo, onClick: editor.redo })
797
833
  ] })
798
834
  ] }) }),
799
- editor.loadState === "success" ? /* @__PURE__ */ jsx5(Banner, { tone: "success", children: i18n.t("success") }) : null,
800
- notice ? /* @__PURE__ */ jsx5(Banner, { tone: notice === "published" ? "success" : "critical", children: i18n.t(notice) }) : null,
835
+ editor.loadState === "success" ? /* @__PURE__ */ jsx6(Banner, { tone: "success", children: i18n.t("success") }) : null,
836
+ notice ? /* @__PURE__ */ jsx6(Banner, { tone: notice === "published" ? "success" : "critical", children: i18n.t(notice) }) : null,
801
837
  /* @__PURE__ */ jsxs4("div", { className: "pb-workspace pb-workspace--document", children: [
802
838
  /* @__PURE__ */ jsxs4("nav", { className: "pb-tool-rail", "aria-label": "\u7F16\u8F91\u5668\u5DE5\u5177", children: [
803
- /* @__PURE__ */ jsx5(Button2, { accessibilityLabel: i18n.t("blocks"), icon: LayoutSectionIcon2, pressed: blockView === "blocks", variant: "tertiary", onClick: () => setBlockView("blocks") }),
804
- /* @__PURE__ */ jsx5(Button2, { accessibilityLabel: i18n.t("outline"), icon: MenuIcon2, pressed: blockView === "outline", variant: "tertiary", onClick: () => setBlockView("outline") })
839
+ /* @__PURE__ */ jsx6(Button2, { accessibilityLabel: i18n.t("blocks"), icon: LayoutSectionIcon2, pressed: blockView === "blocks", variant: "tertiary", onClick: () => setBlockView("blocks") }),
840
+ /* @__PURE__ */ jsx6(Button2, { accessibilityLabel: i18n.t("outline"), icon: MenuIcon2, pressed: blockView === "outline", variant: "tertiary", onClick: () => setBlockView("outline") })
805
841
  ] }),
806
842
  /* @__PURE__ */ jsxs4("aside", { className: "pb-left-panel", "aria-label": "PageDocument \u533A\u5757", children: [
807
- /* @__PURE__ */ jsx5(InlineStack2, { align: "space-between", blockAlign: "center", children: /* @__PURE__ */ jsx5(Text2, { as: "h2", variant: "headingSm", children: blockView === "blocks" ? i18n.t("blocks") : i18n.t("outline") }) }),
808
- blockView === "blocks" ? /* @__PURE__ */ jsx5("div", { className: "pb-block-list", "data-testid": "blocks-view", "aria-label": "\u533A\u5757\u7C7B\u578B\u5E93", role: "list", onDrop: cancelLibraryDrop, children: blockTypes.map((type) => /* @__PURE__ */ jsxs4("div", { className: `pb-document-block-row pb-document-block-row--library ${editor.selectedBlock?.type === type ? "pb-document-block-row--selected" : ""}`, "data-block-type": type, "data-selected": editor.selectedBlock?.type === type, role: "listitem", draggable: editor.actionState.canAdd, "aria-label": `${blockTypeLabel(type, registry)}\uFF0C\u62D6\u62FD\u81F3\u753B\u5E03\u4EE5\u6DFB\u52A0${editor.selectedBlock?.type === type ? "\uFF0C\u5F53\u524D\u9009\u4E2D\u7C7B\u578B" : ""}`, onDragStart: (event) => {
843
+ /* @__PURE__ */ jsx6(InlineStack2, { align: "space-between", blockAlign: "center", children: /* @__PURE__ */ jsx6(Text2, { as: "h2", variant: "headingSm", children: blockView === "blocks" ? i18n.t("blocks") : i18n.t("outline") }) }),
844
+ blockView === "blocks" ? /* @__PURE__ */ jsx6("div", { className: "pb-block-list", "data-testid": "blocks-view", "aria-label": "\u533A\u5757\u7C7B\u578B\u5E93", role: "list", onDrop: cancelLibraryDrop, children: blockTypes.map((type) => /* @__PURE__ */ jsxs4("div", { className: `pb-document-block-row pb-document-block-row--library ${editor.selectedBlock?.type === type ? "pb-document-block-row--selected" : ""}`, "data-block-type": type, "data-selected": editor.selectedBlock?.type === type, role: "listitem", draggable: editor.actionState.canAdd, "aria-label": `${blockTypeLabel(type, registry)}\uFF0C\u62D6\u62FD\u81F3\u753B\u5E03\u4EE5\u6DFB\u52A0${editor.selectedBlock?.type === type ? "\uFF0C\u5F53\u524D\u9009\u4E2D\u7C7B\u578B" : ""}`, onDragStart: (event) => {
809
845
  event.dataTransfer.setData("application/x-page-document-block", type);
810
846
  event.dataTransfer.effectAllowed = "copy";
811
847
  setDraggingLibraryType(type);
812
848
  }, onDragEnd: () => setDraggingLibraryType(null), children: [
813
849
  /* @__PURE__ */ jsxs4("span", { className: "pb-library-block-title", children: [
814
- /* @__PURE__ */ jsx5(Text2, { as: "span", variant: "bodySm", fontWeight: "semibold", children: blockTypeLabel(type, registry) }),
815
- /* @__PURE__ */ jsx5("span", { className: "pb-library-block-drag-hint", "aria-hidden": "true", children: /* @__PURE__ */ jsx5(DragHandleIcon2, {}) })
850
+ /* @__PURE__ */ jsx6(Text2, { as: "span", variant: "bodySm", fontWeight: "semibold", children: blockTypeLabel(type, registry) }),
851
+ /* @__PURE__ */ jsx6("span", { className: "pb-library-block-drag-hint", "aria-hidden": "true", children: /* @__PURE__ */ jsx6(DragHandleIcon2, {}) })
816
852
  ] }),
817
- /* @__PURE__ */ jsx5(Text2, { as: "span", variant: "bodySm", tone: "subdued", children: type })
818
- ] }, type)) }) : editor.document.blocks.length === 0 ? /* @__PURE__ */ jsx5(Text2, { as: "p", tone: "subdued", children: i18n.t("empty") }) : /* @__PURE__ */ jsx5("div", { className: "pb-block-list", "data-testid": "outline-view", children: editor.document.blocks.map((block) => /* @__PURE__ */ jsxs4("button", { type: "button", className: `pb-document-block-row pb-document-block-row--library ${block.id === editor.selectedBlockId ? "pb-document-block-row--selected" : ""}`, "aria-pressed": block.id === editor.selectedBlockId, onClick: () => editor.requestCanvasSelection(block.id), children: [
819
- /* @__PURE__ */ jsx5(Text2, { as: "span", variant: "bodySm", fontWeight: "semibold", children: blockLabel(block, registry) }),
820
- /* @__PURE__ */ jsx5(Text2, { as: "span", variant: "bodySm", tone: "subdued", children: block.id })
853
+ /* @__PURE__ */ jsx6(Text2, { as: "span", variant: "bodySm", tone: "subdued", children: type })
854
+ ] }, type)) }) : editor.document.blocks.length === 0 ? /* @__PURE__ */ jsx6(Text2, { as: "p", tone: "subdued", children: i18n.t("empty") }) : /* @__PURE__ */ jsx6("div", { className: "pb-block-list", "data-testid": "outline-view", children: editor.document.blocks.map((block) => /* @__PURE__ */ jsxs4("button", { type: "button", className: `pb-document-block-row pb-document-block-row--library ${block.id === editor.selectedBlockId ? "pb-document-block-row--selected" : ""}`, "aria-pressed": block.id === editor.selectedBlockId, onClick: () => editor.requestCanvasSelection(block.id), children: [
855
+ /* @__PURE__ */ jsx6(Text2, { as: "span", variant: "bodySm", fontWeight: "semibold", children: blockLabel(block, registry) }),
856
+ /* @__PURE__ */ jsx6(Text2, { as: "span", variant: "bodySm", tone: "subdued", children: block.id })
821
857
  ] }, block.id)) })
822
858
  ] }),
823
859
  /* @__PURE__ */ jsxs4("main", { className: "pb-canvas-area", children: [
824
- /* @__PURE__ */ jsx5("div", { className: "pb-canvas-toolbar", children: /* @__PURE__ */ jsx5(ButtonGroup2, { variant: "segmented", children: Object.keys(deviceLabels2).map((device) => /* @__PURE__ */ jsx5(Button2, { pressed: editor.device === device, onClick: () => editor.setDevice(device), children: i18n.t(deviceLabels2[device]) }, device)) }) }),
860
+ /* @__PURE__ */ jsx6("div", { className: "pb-canvas-toolbar", children: /* @__PURE__ */ jsx6(ButtonGroup2, { variant: "segmented", children: Object.keys(deviceLabels2).map((device) => /* @__PURE__ */ jsx6(Button2, { pressed: editor.device === device, onClick: () => editor.setDevice(device), children: i18n.t(deviceLabels2[device]) }, device)) }) }),
825
861
  /* @__PURE__ */ jsxs4("div", { className: "pb-canvas-stage", children: [
826
- /* @__PURE__ */ jsx5("div", { ref: canvasFrameRef, className: `pb-canvas-frame pb-canvas-frame--${editor.device}`, "data-device": editor.device, children: /* @__PURE__ */ jsx5(Puck2.Preview, {}) }),
862
+ /* @__PURE__ */ jsx6("div", { ref: canvasFrameRef, className: `pb-canvas-frame pb-canvas-frame--${editor.device}`, "data-device": editor.device, children: /* @__PURE__ */ jsx6(Puck2.Preview, {}) }),
827
863
  draggingLibraryType ? /* @__PURE__ */ jsxs4("div", { className: "pb-canvas-drop-target", "data-testid": "canvas-drop-target", role: "region", "aria-label": "\u533A\u5757\u6295\u653E\u533A", onDragOver: (event) => event.preventDefault(), onDrop: dropFromLibrary, children: [
828
864
  "\u677E\u5F00\u4EE5\u6DFB\u52A0 ",
829
865
  blockTypeLabel(draggingLibraryType, registry)
830
866
  ] }) : null,
831
867
  editor.selectedBlock ? /* @__PURE__ */ jsxs4("div", { className: "pb-canvas-overlay", "aria-label": `\u5DF2\u9009\u62E9 ${blockLabel(editor.selectedBlock, registry)}`, children: [
832
- /* @__PURE__ */ jsx5("span", { children: blockLabel(editor.selectedBlock, registry) }),
833
- /* @__PURE__ */ jsx5("span", { children: "Selected" })
868
+ /* @__PURE__ */ jsx6("span", { children: blockLabel(editor.selectedBlock, registry) }),
869
+ /* @__PURE__ */ jsx6("span", { children: "Selected" })
834
870
  ] }) : null
835
871
  ] })
836
872
  ] }),
837
873
  /* @__PURE__ */ jsxs4("aside", { className: "pb-right-panel", "aria-label": "PageDocument \u5C5E\u6027", children: [
838
- /* @__PURE__ */ jsx5(Text2, { as: "h2", variant: "headingSm", children: i18n.t("properties") }),
839
- editor.selectedBlock ? /* @__PURE__ */ jsx5(DocumentInspector, { block: editor.selectedBlock, registry, disabled: !editor.actionState.canEdit, onChange: (props) => editor.updateBlockProps(editor.selectedBlock.id, props) }) : /* @__PURE__ */ jsx5(Text2, { as: "p", tone: "subdued", children: i18n.t("selectBlock") })
874
+ /* @__PURE__ */ jsx6(Text2, { as: "h2", variant: "headingSm", children: i18n.t("properties") }),
875
+ editor.selectedBlock ? /* @__PURE__ */ jsx6(DocumentInspector, { block: editor.selectedBlock, registry, disabled: !editor.actionState.canEdit, onChange: (props) => editor.updateBlockProps(editor.selectedBlock.id, props) }) : /* @__PURE__ */ jsx6(Text2, { as: "p", tone: "subdued", children: i18n.t("selectBlock") })
840
876
  ] })
841
877
  ] })
842
878
  ] })
@@ -844,11 +880,11 @@ function PageDocumentEditor({ iframe = true, registry, adminLocale, onSave, onPu
844
880
  }
845
881
  function CanvasSelectionBridge({ data, requestedBlockId, onCanvasSelected, canvasMutationVersion }) {
846
882
  const puck = usePuck();
847
- const lastSelectedId = useRef3(null);
848
- const lastSyncedData = useRef3(null);
849
- const lastCanvasMutationVersion = useRef3(0);
883
+ const lastSelectedId = useRef4(null);
884
+ const lastSyncedData = useRef4(null);
885
+ const lastCanvasMutationVersion = useRef4(0);
850
886
  const serializedData = JSON.stringify(data);
851
- useEffect2(() => {
887
+ useEffect3(() => {
852
888
  if (lastSyncedData.current === serializedData) return;
853
889
  lastSyncedData.current = serializedData;
854
890
  if (canvasMutationVersion > lastCanvasMutationVersion.current) {
@@ -857,13 +893,13 @@ function CanvasSelectionBridge({ data, requestedBlockId, onCanvasSelected, canva
857
893
  }
858
894
  puck.dispatch({ type: "setData", data });
859
895
  }, [canvasMutationVersion, data, puck, serializedData]);
860
- useEffect2(() => {
896
+ useEffect3(() => {
861
897
  if (!requestedBlockId) return;
862
898
  const selector = puck.getSelectorForId(requestedBlockId);
863
899
  if (selector) puck.dispatch({ type: "setUi", ui: { itemSelector: selector } });
864
900
  }, [puck, requestedBlockId]);
865
901
  const selectedId = typeof puck.selectedItem?.props.id === "string" ? puck.selectedItem.props.id : null;
866
- useEffect2(() => {
902
+ useEffect3(() => {
867
903
  if (selectedId && lastSelectedId.current !== selectedId) {
868
904
  lastSelectedId.current = selectedId;
869
905
  onCanvasSelected(selectedId);
@@ -875,22 +911,62 @@ function EditorStatus({ state }) {
875
911
  const i18n = createAdminI18n();
876
912
  const tone = state === "error" ? "critical" : state === "disabled" ? "warning" : "info";
877
913
  const message = state === "loading" ? i18n.t("loading") : state === "empty" ? i18n.t("empty") : state === "error" ? i18n.t("error") : i18n.t("disabled");
878
- return /* @__PURE__ */ jsx5("div", { className: "pb-editor-status", "data-testid": "page-document-editor-state", "data-editor-state": state, children: /* @__PURE__ */ jsx5(Banner, { tone, title: message, children: state === "disabled" ? i18n.t("disabled") : message }) });
914
+ return /* @__PURE__ */ jsx6("div", { className: "pb-editor-status", "data-testid": "page-document-editor-state", "data-editor-state": state, children: /* @__PURE__ */ jsx6(Banner, { tone, title: message, children: state === "disabled" ? i18n.t("disabled") : message }) });
915
+ }
916
+ function InspectorSection({ title, children, defaultOpen = true }) {
917
+ return /* @__PURE__ */ jsxs4("details", { className: "pb-inspector-section", open: defaultOpen, children: [
918
+ /* @__PURE__ */ jsxs4("summary", { children: [
919
+ /* @__PURE__ */ jsx6("span", { children: title }),
920
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": "true", children: "\u2304" })
921
+ ] }),
922
+ /* @__PURE__ */ jsx6("div", { className: "pb-inspector-section__body", children })
923
+ ] });
924
+ }
925
+ function InspectorTextControl({ label, value, control, disabled, onChange }) {
926
+ const stringValue = typeof value === "string" ? value : "";
927
+ return /* @__PURE__ */ jsx6(TextField, { label, labelHidden: true, value: stringValue, onChange, autoComplete: "off", disabled, multiline: control === "textarea" ? 4 : false, type: control === "url" ? "url" : "text" });
928
+ }
929
+ function InspectorField({ name, field, value, registry, disabled, onChange }) {
930
+ const label = field.label ?? name;
931
+ const Field = registry?.getField(field.field)?.component;
932
+ return /* @__PURE__ */ jsxs4("div", { className: "pb-inspector-field", "data-control": field.control ?? "custom", children: [
933
+ /* @__PURE__ */ jsxs4("div", { className: "pb-inspector-field__heading", children: [
934
+ /* @__PURE__ */ jsx6(Text2, { as: "p", variant: "bodySm", fontWeight: "semibold", children: label }),
935
+ field.description ? /* @__PURE__ */ jsx6(Text2, { as: "p", variant: "bodySm", tone: "subdued", children: field.description }) : null
936
+ ] }),
937
+ field.control ? /* @__PURE__ */ jsx6(InspectorTextControl, { label, value, control: field.control, disabled, onChange: (next) => onChange(next) }) : Field ? /* @__PURE__ */ jsx6(Field, { value, onChange }) : null
938
+ ] });
939
+ }
940
+ function inspectorFieldConfig(name, field) {
941
+ const key = name.toLowerCase();
942
+ const group = /(?:href|url)/.test(key) ? "Links" : /(?:default|shipment|query|hide)/.test(key) ? "Tracking settings" : /(?:id|variant|theme)/.test(key) ? "Advanced" : "Content";
943
+ const description = field.description ?? (field.control === "textarea" ? "\u9002\u5408\u8F83\u957F\u6216\u591A\u884C\u7684\u5C55\u793A\u6587\u6848\u3002" : field.control === "url" ? "\u4F7F\u7528\u7AD9\u5185\u76F8\u5BF9\u8DEF\u5F84\u6216 HTTPS \u5730\u5740\u3002" : key.includes("shipment") ? "\u591A\u4E2A\u5305\u88F9\u6807\u7B7E\u4F7F\u7528 | \u5206\u9694\u3002" : key.includes("default") ? "\u4EC5\u7528\u4E8E\u7F16\u8F91\u5668\u548C\u7A7A\u72B6\u6001\u9884\u89C8\u3002" : void 0);
944
+ return { ...field, group: field.group ?? group, description };
879
945
  }
880
946
  function DocumentInspector({ block, registry, disabled, onChange }) {
881
947
  const definition = registry?.getBlock(block.type);
882
- return /* @__PURE__ */ jsxs4(BlockStack2, { gap: "300", "data-testid": "document-inspector", children: [
883
- /* @__PURE__ */ jsx5(Badge2, { children: block.type }),
884
- /* @__PURE__ */ jsx5(Text2, { as: "p", variant: "headingSm", children: blockLabel(block, registry) }),
885
- block.type === "core.text" ? /* @__PURE__ */ jsx5(TextField, { label: "\u6587\u672C\u5185\u5BB9", value: typeof block.props.content === "string" ? block.props.content : "", onChange: (content) => onChange({ content }), autoComplete: "off", multiline: 4, disabled }) : null,
886
- block.type === "core.image" ? /* @__PURE__ */ jsxs4(Fragment, { children: [
887
- /* @__PURE__ */ jsx5(TextField, { label: "\u56FE\u7247 URL", value: typeof block.props.src === "string" ? block.props.src : "", onChange: (src) => onChange({ src }), autoComplete: "off", disabled }),
888
- /* @__PURE__ */ jsx5(TextField, { label: "\u66FF\u4EE3\u6587\u672C", value: typeof block.props.alt === "string" ? block.props.alt : "", onChange: (alt) => onChange({ alt }), autoComplete: "off", disabled })
948
+ const groupedFields = definition ? Object.entries(definition.fields).reduce((groups, entry) => {
949
+ const [name, field] = entry;
950
+ const configuredField = inspectorFieldConfig(name, field);
951
+ const group = configuredField.group ?? "Content";
952
+ (groups[group] ??= []).push([name, configuredField]);
953
+ return groups;
954
+ }, {}) : {};
955
+ return /* @__PURE__ */ jsxs4("div", { className: "pb-inspector", "data-testid": "document-inspector", children: [
956
+ /* @__PURE__ */ jsxs4("header", { className: "pb-inspector__header", children: [
957
+ /* @__PURE__ */ jsx6(Badge2, { children: block.type }),
958
+ /* @__PURE__ */ jsxs4("div", { children: [
959
+ /* @__PURE__ */ jsx6(Text2, { as: "p", variant: "headingSm", children: blockLabel(block, registry) }),
960
+ /* @__PURE__ */ jsx6(Text2, { as: "p", variant: "bodySm", tone: "subdued", children: definition?.category ?? "Core block" })
961
+ ] })
962
+ ] }),
963
+ block.type === "core.text" ? /* @__PURE__ */ jsx6(InspectorSection, { title: "Content", children: /* @__PURE__ */ jsx6(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,
964
+ block.type === "core.image" ? /* @__PURE__ */ jsxs4(InspectorSection, { title: "Image", children: [
965
+ /* @__PURE__ */ jsx6(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 }) }),
966
+ /* @__PURE__ */ jsx6(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 }) })
889
967
  ] }) : null,
890
- definition ? Object.entries(definition.fields).map(([name, field]) => {
891
- const Field = registry?.getField(field.field)?.component;
892
- return Field ? /* @__PURE__ */ jsx5(Field, { value: block.props[name], onChange: (value) => onChange({ [name]: value }) }, name) : null;
893
- }) : null
968
+ Object.entries(groupedFields).map(([group, fields]) => /* @__PURE__ */ jsx6(InspectorSection, { title: group, defaultOpen: group !== "Advanced", children: fields.map(([name, field]) => /* @__PURE__ */ jsx6(InspectorField, { name, field, value: block.props[name], registry, disabled, onChange: (value) => onChange({ ...block.props, [name]: value }) }, name)) }, group)),
969
+ definition ? /* @__PURE__ */ jsx6("p", { className: "pb-inspector__hint", children: "\u753B\u5E03\u4E2D\u5E26\u865A\u7EBF\u8FB9\u6846\u7684\u5185\u5BB9\u53EF\u76F4\u63A5\u7F16\u8F91\u3002" }) : null
894
970
  ] });
895
971
  }
896
972
  export {
@@ -899,13 +975,19 @@ export {
899
975
  ExtensionRegistry,
900
976
  ExtensionRegistryError,
901
977
  PageDocumentEditorShell,
978
+ TemplateRegistry,
902
979
  WebRenderer,
903
980
  createAdminI18n,
904
981
  createExtensionRegistry,
905
982
  createPageDocument,
983
+ createTemplateRegistry,
906
984
  fromEngineData,
985
+ mergeThemeTokens,
907
986
  migratePageDocument,
987
+ normalizeThemeTokens,
988
+ systemThemeTokens,
908
989
  toEngineData,
990
+ toThemeStyle,
909
991
  useEditorContext,
910
992
  validatePageDocument
911
993
  };
@@ -1,6 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ExtensionRegistry } from './extensions.js';
3
3
  import { PageDocument } from './schema.js';
4
+ import './theme.js';
4
5
 
5
6
  type WebRendererProps = {
6
7
  document: PageDocument;
package/dist/renderer.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  WebRenderer
3
- } from "./chunk-72MFV6J7.js";
3
+ } from "./chunk-KYXIXIWK.js";
4
+ import "./chunk-YK2AUHF6.js";
4
5
  export {
5
6
  WebRenderer
6
7
  };
@@ -0,0 +1,5 @@
1
+ export { WebRenderer, WebRendererProps } from './renderer.js';
2
+ export { BlockDefinition, BlockEditorProps, BlockVariantDefinition, DataSourceDefinition, EditorAction, EditorActionPosition, ExtensionActionContext, ExtensionRegistry, ExtensionRegistryError, ExtensionRegistryErrorCode, ExtensionRegistryOptions, ExtensionTarget, FieldConfig, FieldDefinition, FieldProps, LifecycleHooks, PageBuilderExtension, RendererDefinition, TemplateDefinition, TemplateRegistry, TemplateSource, UISlotContribution, UISlotName, ValidationIssue, createExtensionRegistry, createTemplateRegistry } from './extensions.js';
3
+ export { BlockNode, BlockStyleOverrides, DataBinding, JsonPrimitive, JsonValue, PageDocument, PageDocumentIssue, PageDocumentMigration, PageDocumentSchemaVersion, PageDocumentValidation, PageSettings, RenderTarget, createPageDocument, migratePageDocument, validatePageDocument } from './schema.js';
4
+ export { ThemeTokenName, ThemeTokenValidation, ThemeTokens, mergeThemeTokens, normalizeThemeTokens, systemThemeTokens, toThemeStyle } from './theme.js';
5
+ import 'react';
@@ -0,0 +1,36 @@
1
+ import {
2
+ ExtensionRegistry,
3
+ ExtensionRegistryError,
4
+ TemplateRegistry,
5
+ createExtensionRegistry,
6
+ createTemplateRegistry
7
+ } from "./chunk-PUTYZI7B.js";
8
+ import {
9
+ WebRenderer
10
+ } from "./chunk-KYXIXIWK.js";
11
+ import {
12
+ createPageDocument,
13
+ migratePageDocument,
14
+ validatePageDocument
15
+ } from "./chunk-OBN4DMOR.js";
16
+ import {
17
+ mergeThemeTokens,
18
+ normalizeThemeTokens,
19
+ systemThemeTokens,
20
+ toThemeStyle
21
+ } from "./chunk-YK2AUHF6.js";
22
+ export {
23
+ ExtensionRegistry,
24
+ ExtensionRegistryError,
25
+ TemplateRegistry,
26
+ WebRenderer,
27
+ createExtensionRegistry,
28
+ createPageDocument,
29
+ createTemplateRegistry,
30
+ mergeThemeTokens,
31
+ migratePageDocument,
32
+ normalizeThemeTokens,
33
+ systemThemeTokens,
34
+ toThemeStyle,
35
+ validatePageDocument
36
+ };
package/dist/schema.d.ts CHANGED
@@ -1,8 +1,11 @@
1
+ import { ThemeTokens } from './theme.js';
2
+
1
3
  type JsonPrimitive = string | number | boolean | null;
2
4
  type JsonValue = JsonPrimitive | JsonValue[] | {
3
5
  [key: string]: JsonValue;
4
6
  };
5
7
  type RenderTarget = "web" | "email";
8
+ type PageDocumentSchemaVersion = 1;
6
9
  type PageSettings = {
7
10
  locale: string;
8
11
  seoTitle?: string;
@@ -11,19 +14,25 @@ type DataBinding = {
11
14
  source: string;
12
15
  params?: Record<string, JsonValue>;
13
16
  };
17
+ /** Token-only block styling. Arbitrary CSS properties are deliberately not persisted. */
18
+ type BlockStyleOverrides = ThemeTokens;
14
19
  type BlockNode = {
15
20
  id: string;
16
21
  type: string;
17
22
  version: number;
18
23
  props: Record<string, JsonValue>;
24
+ variant: string;
25
+ style: BlockStyleOverrides;
19
26
  slots?: Record<string, BlockNode[]>;
20
27
  binding?: DataBinding;
21
28
  };
22
29
  type PageDocument = {
23
- schemaVersion: 1;
30
+ schemaVersion: PageDocumentSchemaVersion;
24
31
  pageId: string;
25
32
  target: RenderTarget;
26
33
  templateId?: string;
34
+ templateVersion?: number;
35
+ theme: ThemeTokens;
27
36
  root: Record<string, JsonValue>;
28
37
  blocks: BlockNode[];
29
38
  settings: PageSettings;
@@ -47,13 +56,13 @@ type PageDocumentMigration = {
47
56
  success: false;
48
57
  issues: PageDocumentIssue[];
49
58
  };
50
- declare function createPageDocument(input: Partial<PageDocument> & Pick<PageDocument, "pageId">): PageDocument;
59
+ declare function createPageDocument(input: Omit<Partial<PageDocument>, "schemaVersion" | "blocks" | "theme"> & {
60
+ pageId: string;
61
+ blocks?: Array<Omit<BlockNode, "variant" | "style"> & Partial<Pick<BlockNode, "variant" | "style">>>;
62
+ theme?: ThemeTokens;
63
+ }): PageDocument;
51
64
  declare function validatePageDocument(value: unknown): PageDocumentValidation;
52
- /**
53
- * Accept the pre-versioned shape produced by the early Demo and normalize it
54
- * to the first persisted PageDocument schema. Future schema migrations belong
55
- * here so storage callers have one validation boundary.
56
- */
65
+ /** V0.6.1 starts from this schema; no pre-integration document compatibility is needed. */
57
66
  declare function migratePageDocument(value: unknown): PageDocumentMigration;
58
67
 
59
- export { type BlockNode, type DataBinding, type JsonPrimitive, type JsonValue, type PageDocument, type PageDocumentIssue, type PageDocumentMigration, type PageDocumentValidation, type PageSettings, type RenderTarget, createPageDocument, migratePageDocument, validatePageDocument };
68
+ export { type BlockNode, type BlockStyleOverrides, type DataBinding, type JsonPrimitive, type JsonValue, type PageDocument, type PageDocumentIssue, type PageDocumentMigration, type PageDocumentSchemaVersion, type PageDocumentValidation, type PageSettings, type RenderTarget, createPageDocument, migratePageDocument, validatePageDocument };
package/dist/schema.js CHANGED
@@ -2,7 +2,8 @@ import {
2
2
  createPageDocument,
3
3
  migratePageDocument,
4
4
  validatePageDocument
5
- } from "./chunk-G2LNCKYO.js";
5
+ } from "./chunk-OBN4DMOR.js";
6
+ import "./chunk-YK2AUHF6.js";
6
7
  export {
7
8
  createPageDocument,
8
9
  migratePageDocument,
@@ -0,0 +1,14 @@
1
+ type ThemeTokenName = "color.background" | "color.surface" | "color.text" | "color.muted" | "color.primary" | "color.border" | "font.family" | "font.size" | "radius" | "spacing";
2
+ type ThemeTokens = Partial<Record<ThemeTokenName, string>>;
3
+ type ThemeTokenValidation = {
4
+ success: true;
5
+ data: ThemeTokens;
6
+ } | {
7
+ success: false;
8
+ };
9
+ declare const systemThemeTokens: Readonly<Required<ThemeTokens>>;
10
+ declare function normalizeThemeTokens(value: unknown): ThemeTokenValidation;
11
+ declare function mergeThemeTokens(...layers: Array<ThemeTokens | undefined>): Required<ThemeTokens>;
12
+ declare function toThemeStyle(tokens: ThemeTokens): Record<string, string>;
13
+
14
+ export { type ThemeTokenName, type ThemeTokenValidation, type ThemeTokens, mergeThemeTokens, normalizeThemeTokens, systemThemeTokens, toThemeStyle };
package/dist/theme.js ADDED
@@ -0,0 +1,12 @@
1
+ import {
2
+ mergeThemeTokens,
3
+ normalizeThemeTokens,
4
+ systemThemeTokens,
5
+ toThemeStyle
6
+ } from "./chunk-YK2AUHF6.js";
7
+ export {
8
+ mergeThemeTokens,
9
+ normalizeThemeTokens,
10
+ systemThemeTokens,
11
+ toThemeStyle
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@standhigher/puck-page-builder",
3
- "version": "0.1.0",
3
+ "version": "0.8.0",
4
4
  "description": "React PageDocument editor, extension registry, and web renderer for BestTrack page experiences.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -40,6 +40,14 @@
40
40
  "types": "./dist/schema.d.ts",
41
41
  "import": "./dist/schema.js"
42
42
  },
43
+ "./runtime": {
44
+ "types": "./dist/runtime.d.ts",
45
+ "import": "./dist/runtime.js"
46
+ },
47
+ "./theme": {
48
+ "types": "./dist/theme.d.ts",
49
+ "import": "./dist/theme.js"
50
+ },
43
51
  "./styles.css": "./src/styles.css"
44
52
  },
45
53
  "publishConfig": {
@@ -62,7 +70,7 @@
62
70
  "typescript": "^5.9.3"
63
71
  },
64
72
  "scripts": {
65
- "build": "tsup src/index.ts src/renderer.ts src/extensions.ts src/schema.ts --format esm --dts --clean",
73
+ "build": "tsup src/index.ts src/renderer.ts src/runtime.ts src/extensions.ts src/schema.ts src/theme.ts --format esm --dts --clean && node scripts/assert-runtime-boundary.mjs",
66
74
  "typecheck": "tsc --noEmit"
67
75
  }
68
76
  }
package/src/styles.css CHANGED
@@ -13,7 +13,30 @@ button:focus-visible, [role="button"]:focus-visible { outline: 2px solid var(--b
13
13
  .pb-tool-rail .Polaris-Button { min-width: 44px; min-height: 44px; }
14
14
  .pb-left-panel, .pb-right-panel { display: flex; flex-direction: column; gap: var(--builder-space-4); min-width: 0; padding: var(--builder-space-4); overflow: auto; background: var(--builder-surface); }
15
15
  .pb-left-panel { border-right: 1px solid var(--builder-border); }
16
- .pb-right-panel { border-left: 1px solid var(--builder-border); }
16
+ .pb-right-panel { border-left: 1px solid var(--builder-border); background: #f8fafc; }
17
+ .pb-right-panel > h2 { margin: 0; color: #475569; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; }
18
+ .pb-inspector { display: grid; gap: var(--builder-space-3); }
19
+ .pb-inspector__header { display: grid; gap: var(--builder-space-2); padding: var(--builder-space-3); border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; box-shadow: 0 1px 2px rgb(15 23 42 / 4%); }
20
+ .pb-inspector__header .Polaris-Badge { width: fit-content; max-width: 100%; overflow: hidden; color: #1d4ed8; background: #eff6ff; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
21
+ .pb-inspector__header p { margin: 0; }
22
+ .pb-inspector-section { overflow: hidden; border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; box-shadow: 0 1px 2px rgb(15 23 42 / 3%); }
23
+ .pb-inspector-section > summary { display: flex; align-items: center; justify-content: space-between; min-height: 44px; padding: 0 var(--builder-space-3); color: #0f172a; font-size: 13px; font-weight: 650; cursor: pointer; list-style: none; }
24
+ .pb-inspector-section > summary::-webkit-details-marker { display: none; }
25
+ .pb-inspector-section > summary > span:last-child { color: #64748b; font-size: 16px; transition: transform 140ms ease; }
26
+ .pb-inspector-section:not([open]) > summary > span:last-child { transform: rotate(-90deg); }
27
+ .pb-inspector-section__body { display: grid; gap: var(--builder-space-3); padding: 0 var(--builder-space-3) var(--builder-space-3); border-top: 1px solid #f1f5f9; }
28
+ .pb-inspector-field { display: grid; gap: 6px; }
29
+ .pb-inspector-field__heading { display: grid; gap: 2px; }
30
+ .pb-inspector-field__heading p { margin: 0; }
31
+ .pb-inspector-field__heading p:first-child { color: #334155; font-size: 13px; }
32
+ .pb-inspector-field__heading p:last-child { color: #64748b; font-size: 12px; line-height: 1.45; }
33
+ .pb-inspector-field input, .pb-inspector-field textarea { width: 100%; min-height: 40px; padding: 9px 11px; border: 1px solid #cbd5e1; border-radius: 8px; outline: 0; color: #0f172a; background: #fff; font: inherit; font-size: 14px; line-height: 1.4; transition: border-color 140ms ease, box-shadow 140ms ease; }
34
+ .pb-inspector-field textarea { min-height: 96px; resize: vertical; }
35
+ .pb-inspector-field input[type="url"] { color: #334155; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
36
+ .pb-inspector-field input:hover, .pb-inspector-field textarea:hover { border-color: #94a3b8; }
37
+ .pb-inspector-field input:focus, .pb-inspector-field textarea:focus { border-color: #2563eb; box-shadow: 0 0 0 3px rgb(37 99 235 / 12%); }
38
+ .pb-inspector-field input:disabled, .pb-inspector-field textarea:disabled { color: #94a3b8; background: #f8fafc; cursor: not-allowed; }
39
+ .pb-inspector__hint { margin: 0; padding: 0 var(--builder-space-2); color: #64748b; font-size: 12px; line-height: 1.5; }
17
40
  .pb-panel--closed { display: none; }
18
41
  .pb-block-list { display: flex; flex: 1; flex-direction: column; gap: var(--builder-space-2); min-height: 0; }
19
42
  .pb-block-row { display: flex; align-items: center; gap: var(--builder-space-2); padding: var(--builder-space-2); border: 1px solid transparent; border-radius: var(--builder-panel-radius); background: transparent; transition: background-color 120ms ease, border-color 120ms ease; }
@@ -1,15 +0,0 @@
1
- // src/renderer/web/WebRenderer.tsx
2
- import { jsx } from "react/jsx-runtime";
3
- function WebRenderer({ document, className, registry }) {
4
- return /* @__PURE__ */ jsx("main", { className: className ?? "pb-web-renderer", "data-page-id": document.pageId, lang: document.settings.locale, children: document.blocks.map((block) => {
5
- if (block.type === "core.text") return /* @__PURE__ */ jsx("section", { "data-block-id": block.id, className: "pb-web-renderer__text", children: /* @__PURE__ */ jsx("p", { children: typeof block.props.content === "string" ? block.props.content : "" }) }, block.id);
6
- if (block.type === "core.image") return /* @__PURE__ */ jsx("figure", { "data-block-id": block.id, className: "pb-web-renderer__image", children: /* @__PURE__ */ jsx("img", { src: typeof block.props.src === "string" ? block.props.src : "", alt: typeof block.props.alt === "string" ? block.props.alt : "" }) }, block.id);
7
- const BlockRenderer = registry?.getBlock(block.type)?.render.web;
8
- if (BlockRenderer) return /* @__PURE__ */ jsx("section", { "data-block-id": block.id, children: /* @__PURE__ */ jsx(BlockRenderer, { ...block.props }) }, block.id);
9
- return null;
10
- }) });
11
- }
12
-
13
- export {
14
- WebRenderer
15
- };