@standhigher/puck-page-builder 0.1.0 → 0.7.2

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
2
  import { RenderTarget, 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";
@@ -26,9 +27,16 @@ interface BlockDefinition<P = Record<string, unknown>> {
26
27
  defaultProps: P;
27
28
  fields: Record<string, FieldConfig>;
28
29
  render: Partial<Record<RenderTarget, ComponentType<P>>>;
30
+ defaultVariant?: string;
31
+ variants?: BlockVariantDefinition[];
29
32
  dataSources?: string[];
30
33
  validate?: (props: P) => ValidationIssue[];
31
34
  }
35
+ interface BlockVariantDefinition {
36
+ id: string;
37
+ label: string;
38
+ theme?: ThemeTokens;
39
+ }
32
40
  interface FieldDefinition<T = unknown> {
33
41
  type: string;
34
42
  component: ComponentType<FieldProps<T>>;
@@ -65,13 +73,18 @@ interface DataSourceDefinition<P = any, R = unknown> {
65
73
  live: (params: P) => Promise<R>;
66
74
  validateParams?: (params: P) => ValidationIssue[];
67
75
  }
76
+ type TemplateSource = "built-in" | "marketplace" | "custom";
68
77
  interface TemplateDefinition {
69
78
  id: string;
70
79
  version: number;
71
80
  name: string;
72
81
  target: RenderTarget;
82
+ source: TemplateSource;
73
83
  thumbnail?: string;
74
84
  create(): PageDocument;
85
+ /** Block types that must be registered before this template can be used. */
86
+ requiredBlocks?: string[];
87
+ theme?: ThemeTokens;
75
88
  }
76
89
  interface LifecycleHooks {
77
90
  onChange?(document: PageDocument): void;
@@ -106,7 +119,7 @@ type ExtensionRegistryOptions = {
106
119
  disabled?: string[];
107
120
  };
108
121
 
109
- type ExtensionRegistryErrorCode = "duplicate-extension" | "duplicate-definition" | "invalid-identifier" | "invalid-target" | "missing-dependency" | "dependency-cycle";
122
+ type ExtensionRegistryErrorCode = "duplicate-extension" | "duplicate-definition" | "invalid-identifier" | "invalid-target" | "missing-dependency" | "missing-template-dependency" | "dependency-cycle";
110
123
  declare class ExtensionRegistryError extends Error {
111
124
  readonly code: ExtensionRegistryErrorCode;
112
125
  constructor(code: ExtensionRegistryErrorCode, message: string);
@@ -114,6 +127,14 @@ declare class ExtensionRegistryError extends Error {
114
127
  type Registered<T> = T & {
115
128
  extension: string;
116
129
  };
130
+ /** Immutable template view with explicit source and block dependency validation. */
131
+ declare class TemplateRegistry {
132
+ private readonly templateMap;
133
+ private constructor();
134
+ get templates(): readonly Registered<TemplateDefinition>[];
135
+ get(id: string): Registered<TemplateDefinition> | undefined;
136
+ static create(templates: readonly Registered<TemplateDefinition>[], blocks: readonly BlockDefinition[]): TemplateRegistry;
137
+ }
117
138
  /** Immutable compiled view of all enabled extensions. */
118
139
  declare class ExtensionRegistry {
119
140
  readonly extensions: readonly PageBuilderExtension[];
@@ -123,6 +144,7 @@ declare class ExtensionRegistry {
123
144
  private readonly rendererMap;
124
145
  private readonly dataSourceMap;
125
146
  private readonly templateMap;
147
+ readonly templateRegistry: TemplateRegistry;
126
148
  private readonly slotMap;
127
149
  private readonly hooks;
128
150
  private constructor();
@@ -144,5 +166,6 @@ declare class ExtensionRegistry {
144
166
  static create(extensions: PageBuilderExtension[], options?: ExtensionRegistryOptions): ExtensionRegistry;
145
167
  }
146
168
  declare function createExtensionRegistry(extensions: PageBuilderExtension[], options?: ExtensionRegistryOptions): ExtensionRegistry;
169
+ declare function createTemplateRegistry(templates: TemplateDefinition[], blocks?: BlockDefinition[]): TemplateRegistry;
147
170
 
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 };
171
+ export { type BlockDefinition, 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, 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";
@@ -399,7 +407,7 @@ function toEngineBlock(block, registry) {
399
407
  }
400
408
  function toEngineData(document, registry) {
401
409
  return {
402
- root: { ...document.root },
410
+ root: { props: { ...document.root } },
403
411
  content: document.blocks.map((block) => toEngineBlock(block, registry))
404
412
  };
405
413
  }
@@ -416,6 +424,8 @@ function fromEngineData(data, base, registry) {
416
424
  type,
417
425
  version: previous?.version ?? 1,
418
426
  props,
427
+ variant: previous?.variant ?? registry?.getBlock(type)?.defaultVariant ?? "default",
428
+ style: previous?.style ?? {},
419
429
  ...previous?.slots ? { slots: previous.slots } : {},
420
430
  ...previous?.binding ? { binding: previous.binding } : {}
421
431
  }];
@@ -452,11 +462,11 @@ function uniqueBlockId(type, blocks) {
452
462
  return `${prefix}-${index}`;
453
463
  }
454
464
  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: "" } };
465
+ if (type === "core.text") return { id: uniqueBlockId(type, blocks), type, version: 1, props: { content: "New text block" }, variant: "default", style: {} };
466
+ 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
467
  const definition = registry?.getBlock(type);
458
468
  if (!definition) throw new Error(`Unknown PageDocument block type: ${type}`);
459
- return { id: uniqueBlockId(type, blocks), type, version: definition.version, props: definition.defaultProps };
469
+ return { id: uniqueBlockId(type, blocks), type, version: definition.version, props: definition.defaultProps, variant: definition.defaultVariant ?? "default", style: {} };
460
470
  }
461
471
  function EditorProvider({ initialDocument, registry, loadState = "ready", leaveWarning = "You have unsaved changes.", onDocumentChange, children }) {
462
472
  const [history, dispatch] = useReducer(historyReducer, initialDocument, (document) => ({ document, selectedBlockId: document.blocks[0]?.id ?? null, past: [], future: [], device: "desktop", savedDocument: document }));
@@ -889,7 +899,7 @@ function DocumentInspector({ block, registry, disabled, onChange }) {
889
899
  ] }) : null,
890
900
  definition ? Object.entries(definition.fields).map(([name, field]) => {
891
901
  const Field = registry?.getField(field.field)?.component;
892
- return Field ? /* @__PURE__ */ jsx5(Field, { value: block.props[name], onChange: (value) => onChange({ [name]: value }) }, name) : null;
902
+ return Field ? /* @__PURE__ */ jsx5(Field, { value: block.props[name], onChange: (value) => onChange({ ...block.props, [name]: value }) }, name) : null;
893
903
  }) : null
894
904
  ] });
895
905
  }
@@ -899,13 +909,19 @@ export {
899
909
  ExtensionRegistry,
900
910
  ExtensionRegistryError,
901
911
  PageDocumentEditorShell,
912
+ TemplateRegistry,
902
913
  WebRenderer,
903
914
  createAdminI18n,
904
915
  createExtensionRegistry,
905
916
  createPageDocument,
917
+ createTemplateRegistry,
906
918
  fromEngineData,
919
+ mergeThemeTokens,
907
920
  migratePageDocument,
921
+ normalizeThemeTokens,
922
+ systemThemeTokens,
908
923
  toEngineData,
924
+ toThemeStyle,
909
925
  useEditorContext,
910
926
  validatePageDocument
911
927
  };
@@ -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, 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.7.2",
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
  }
@@ -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
- };