@nextbridgehq/payload-block-builder 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,18 @@ A visual block builder plugin for Payload v3. Design your content blocks through
14
14
  - **Evolving content schemas:** Roll out new block versions without breaking content that was built against older ones.
15
15
  - **Headless frontends:** Fetch structured block data from the Payload API and render it with any framework.
16
16
 
17
+ ## Database compatibility
18
+
19
+ Works with all Payload-supported databases — no direct SQL, no database-specific code:
20
+
21
+ | Database | Adapter |
22
+ |---|---|
23
+ | PostgreSQL / Supabase / Neon | `@payloadcms/db-postgres` |
24
+ | SQLite / Turso / LibSQL | `@payloadcms/db-sqlite` |
25
+ | MongoDB | `@payloadcms/db-mongodb` |
26
+
27
+ ---
28
+
17
29
  ## Quick start
18
30
 
19
31
  ### Option A — Automatic setup (recommended)
@@ -77,11 +89,19 @@ export default buildConfig({
77
89
 
78
90
  ```tsx
79
91
  import React from 'react'
92
+ import { headers } from 'next/headers'
93
+ import { redirect } from 'next/navigation'
94
+ import { getPayload } from 'payload'
95
+ import config from '@payload-config'
80
96
  import '@nextbridgehq/payload-block-builder/builder.css'
81
97
 
82
98
  export const metadata = { title: 'Block Builder' }
83
99
 
84
- export default function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
100
+ export default async function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
101
+ const payload = await getPayload({ config })
102
+ const { user } = await payload.auth({ headers: await headers() })
103
+ if (!user) redirect('/admin/login')
104
+
85
105
  return (
86
106
  <html lang="en">
87
107
  <body style={{ margin: 0, padding: 0, height: '100vh', overflow: 'hidden' }}>
@@ -186,6 +206,60 @@ Render each block type however you like — a switch statement or a component ma
186
206
 
187
207
  ---
188
208
 
209
+ ## Supported field types
210
+
211
+ These field types are available in the block builder and render correctly in the admin field UI:
212
+
213
+ | Type | Description |
214
+ |---|---|
215
+ | `text` | Single-line text input |
216
+ | `textarea` | Multi-line text input |
217
+ | `number` | Numeric input |
218
+ | `email` | Email address |
219
+ | `date` | Date picker |
220
+ | `checkbox` | Boolean toggle |
221
+ | `select` | Dropdown with custom options |
222
+ | `radio` | Radio button group with custom options |
223
+ | `upload` | File / image picker (from the media collection) |
224
+ | `relationship` | Document picker from any collection |
225
+ | `json` | Raw JSON data |
226
+
227
+ ---
228
+
229
+ ## Using `dbLayoutField` directly
230
+
231
+ If you prefer not to use the plugin's `collections` option, you can add the layout tab manually to any collection:
232
+
233
+ ```ts
234
+ import { dbLayoutField } from '@nextbridgehq/payload-block-builder'
235
+
236
+ export const Pages: CollectionConfig = {
237
+ slug: 'pages',
238
+ fields: [
239
+ {
240
+ type: 'tabs',
241
+ tabs: [
242
+ { label: 'Content', fields: [] },
243
+ dbLayoutField(), // fieldName='dbLayout', tab label='DB Layout'
244
+ dbLayoutField('heroBlocks', 'Hero'), // custom field name and tab label
245
+ ],
246
+ },
247
+ ],
248
+ }
249
+ ```
250
+
251
+ ---
252
+
253
+ ## CSS imports reference
254
+
255
+ | Import path | Purpose |
256
+ |---|---|
257
+ | `@nextbridgehq/payload-block-builder/builder.css` | Block Builder UI page styles |
258
+ | `@nextbridgehq/payload-block-builder/block-data-field.css` | DB Layout field styles in admin |
259
+ | `@nextbridgehq/payload-block-builder/schema-builder-field.css` | Schema Builder field styles in admin |
260
+
261
+ ---
262
+
189
263
  ## License
190
264
 
191
265
  MIT © [Nextbridge](https://nextbridge.com)
package/dist/bin/init.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
 
4
3
  // src/bin/init.ts
5
4
  import fs from "fs";
@@ -13,13 +12,21 @@ export default function BlockBuilderPage() {
13
12
  }
14
13
  `;
15
14
  var LAYOUT_CONTENT = `import React from 'react'
15
+ import { headers } from 'next/headers'
16
+ import { redirect } from 'next/navigation'
17
+ import { getPayload } from 'payload'
18
+ import config from '@payload-config'
16
19
  import '@nextbridgehq/payload-block-builder/builder.css'
17
20
 
18
21
  export const metadata = {
19
22
  title: 'Block Builder',
20
23
  }
21
24
 
22
- export default function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
25
+ export default async function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
26
+ const payload = await getPayload({ config })
27
+ const { user } = await payload.auth({ headers: await headers() })
28
+ if (!user) redirect('/admin/login')
29
+
23
30
  return (
24
31
  <html lang="en">
25
32
  <body style={{ margin: 0, padding: 0, height: '100vh', overflow: 'hidden' }}>
@@ -76,7 +83,7 @@ function addImport(content) {
76
83
  while ((m = lastFromRegex.exec(content)) !== null) lastMatch = m;
77
84
  if (!lastMatch) return newImport + "\n" + content;
78
85
  const insertPos = lastMatch.index + lastMatch[0].length;
79
- return content.slice(0, insertPos) + "\n" + newImport + content.slice(insertPos);
86
+ return content.slice(0, insertPos) + "\n" + newImport + "\n" + content.slice(insertPos);
80
87
  }
81
88
  function insertIntoPluginsArray(content, collectionsArg) {
82
89
  const pluginsMatch = /\bplugins\s*:\s*\[/.exec(content);
@@ -84,13 +91,19 @@ function insertIntoPluginsArray(content, collectionsArg) {
84
91
  const openPos = content.indexOf("[", pluginsMatch.index);
85
92
  const closePos = findClosingBracket(content, openPos);
86
93
  if (closePos === -1) return null;
94
+ const beforePlugins = content.slice(0, pluginsMatch.index);
95
+ const pluginsLineStart = beforePlugins.lastIndexOf("\n") + 1;
96
+ const outerIndent = content.slice(pluginsLineStart, pluginsMatch.index).match(/^([ \t]*)/)?.[1] ?? " ";
97
+ const entryIndent = outerIndent + " ";
87
98
  const beforeClose = content.slice(0, closePos);
88
99
  const prevNL = beforeClose.lastIndexOf("\n");
89
- const closingIndent = beforeClose.slice(prevNL + 1).match(/^([ \t]*)/)?.[1] ?? " ";
90
- const entryIndent = closingIndent + " ";
91
- const newLine = `${entryIndent}dynamicBlocksPlugin({ collections: [${collectionsArg}] }),
92
- `;
93
- return content.slice(0, prevNL + 1) + newLine + content.slice(prevNL + 1);
100
+ const isSingleLine = prevNL < openPos;
101
+ const newEntry = `${entryIndent}dynamicBlocksPlugin({ collections: [${collectionsArg}] }),`;
102
+ if (isSingleLine) {
103
+ return content.slice(0, openPos + 1) + "\n" + newEntry + "\n" + outerIndent + content.slice(closePos);
104
+ } else {
105
+ return content.slice(0, prevNL + 1) + newEntry + "\n" + content.slice(prevNL + 1);
106
+ }
94
107
  }
95
108
  function injectPluginsBlock(content, collectionsArg) {
96
109
  const collMatch = /\bcollections\s*:\s*\[/.exec(content);
@@ -110,7 +123,13 @@ ${outerIndent}],`;
110
123
  return content.slice(0, afterCollLine) + "\n" + pluginsBlock + content.slice(afterCollLine);
111
124
  }
112
125
  function modifyPayloadConfig(configPath, collectionsArg) {
113
- let content = fs.readFileSync(configPath, "utf8");
126
+ let content;
127
+ try {
128
+ content = fs.readFileSync(configPath, "utf8");
129
+ } catch (err) {
130
+ console.error(`Error: Could not read ${configPath}: ${err.message}`);
131
+ process.exit(1);
132
+ }
114
133
  if (content.includes("dynamicBlocksPlugin")) {
115
134
  console.log(`Skipped: dynamicBlocksPlugin already present in ${configPath}`);
116
135
  return;
@@ -119,8 +138,16 @@ function modifyPayloadConfig(configPath, collectionsArg) {
119
138
  const noComments = content.replace(/\/\/[^\n]*/g, "");
120
139
  const hasPluginsArray = /\bplugins\s*:\s*\[/.test(noComments);
121
140
  const hasPluginsShorthand = /^\s*plugins\s*,/m.test(noComments);
141
+ function writeConfig(data) {
142
+ try {
143
+ fs.writeFileSync(configPath, data, "utf8");
144
+ } catch (err) {
145
+ console.error(`Error: Could not write ${configPath}: ${err.message}`);
146
+ process.exit(1);
147
+ }
148
+ }
122
149
  if (hasPluginsShorthand && !hasPluginsArray) {
123
- fs.writeFileSync(configPath, content, "utf8");
150
+ writeConfig(content);
124
151
  console.log(`Updated: ${configPath} (added import)`);
125
152
  console.log(` Note: 'plugins' is imported from another file.`);
126
153
  console.log(` Add dynamicBlocksPlugin({ collections: ['pages'] }) to that file manually.`);
@@ -133,13 +160,13 @@ function modifyPayloadConfig(configPath, collectionsArg) {
133
160
  result = injectPluginsBlock(content, collectionsArg);
134
161
  }
135
162
  if (result === null) {
136
- fs.writeFileSync(configPath, content, "utf8");
163
+ writeConfig(content);
137
164
  console.log(`Updated: ${configPath} (added import only)`);
138
165
  console.log(` Could not auto-detect plugins array. Add manually:`);
139
166
  console.log(` plugins: [ dynamicBlocksPlugin({ collections: [${collectionsArg}] }) ]`);
140
167
  return;
141
168
  }
142
- fs.writeFileSync(configPath, result, "utf8");
169
+ writeConfig(result);
143
170
  console.log(`Updated: ${configPath} (added dynamicBlocksPlugin)`);
144
171
  }
145
172
  function printNextSteps(dbAdapter) {
@@ -165,7 +192,14 @@ function printNextSteps(dbAdapter) {
165
192
  function main() {
166
193
  const args = process.argv.slice(2);
167
194
  const collectionsFlag = args.find((a) => a.startsWith("--collections="));
168
- const collectionsValue = collectionsFlag ? collectionsFlag.replace("--collections=", "").split(",").map((s) => s.trim()) : ["pages"];
195
+ const rawCollections = collectionsFlag ? collectionsFlag.replace("--collections=", "").split(",").map((s) => s.trim()) : ["pages"];
196
+ const invalidSlugs = rawCollections.filter((c) => !/^[a-z0-9_-]+$/i.test(c));
197
+ if (invalidSlugs.length > 0) {
198
+ console.error(`Error: Invalid collection slug(s): ${invalidSlugs.join(", ")}`);
199
+ console.error("Collection slugs may only contain letters, numbers, hyphens, and underscores.");
200
+ process.exit(1);
201
+ }
202
+ const collectionsValue = rawCollections;
169
203
  const collectionsArg = collectionsValue.map((c) => `'${c}'`).join(", ");
170
204
  const appDir = findAppDir();
171
205
  if (!appDir) {
@@ -173,32 +207,52 @@ function main() {
173
207
  process.exit(1);
174
208
  }
175
209
  const builderDir = path.join(appDir, "block-builder");
176
- if (!fs.existsSync(builderDir)) {
177
- fs.mkdirSync(builderDir, { recursive: true });
210
+ try {
211
+ if (!fs.existsSync(builderDir)) {
212
+ fs.mkdirSync(builderDir, { recursive: true });
213
+ }
214
+ } catch (err) {
215
+ console.error(`Error: Could not create directory ${builderDir}: ${err.message}`);
216
+ process.exit(1);
178
217
  }
179
218
  const pagePath = path.join(builderDir, "page.tsx");
180
219
  const layoutPath = path.join(builderDir, "layout.tsx");
181
220
  if (fs.existsSync(pagePath)) {
182
221
  console.log(`Skipped: ${pagePath} already exists`);
183
222
  } else {
184
- fs.writeFileSync(pagePath, PAGE_CONTENT);
185
- console.log(`Created: ${pagePath}`);
223
+ try {
224
+ fs.writeFileSync(pagePath, PAGE_CONTENT);
225
+ console.log(`Created: ${pagePath}`);
226
+ } catch (err) {
227
+ console.error(`Error: Could not write ${pagePath}: ${err.message}`);
228
+ process.exit(1);
229
+ }
186
230
  }
187
231
  if (fs.existsSync(layoutPath)) {
188
232
  console.log(`Skipped: ${layoutPath} already exists`);
189
233
  } else {
190
- fs.writeFileSync(layoutPath, LAYOUT_CONTENT);
191
- console.log(`Created: ${layoutPath}`);
234
+ try {
235
+ fs.writeFileSync(layoutPath, LAYOUT_CONTENT);
236
+ console.log(`Created: ${layoutPath}`);
237
+ } catch (err) {
238
+ console.error(`Error: Could not write ${layoutPath}: ${err.message}`);
239
+ process.exit(1);
240
+ }
192
241
  }
193
242
  const payloadRouteDir = path.join(appDir, "(payload)");
194
243
  const customScssPath = path.join(payloadRouteDir, "custom.scss");
195
244
  if (fs.existsSync(customScssPath)) {
196
- const existing = fs.readFileSync(customScssPath, "utf8");
197
- if (!existing.includes("@nextbridgehq/payload-block-builder")) {
198
- fs.appendFileSync(customScssPath, "\n" + CUSTOM_SCSS_IMPORTS);
199
- console.log(`Updated: ${customScssPath} (added admin field styles)`);
200
- } else {
201
- console.log(`Skipped: ${customScssPath} already has block-builder imports`);
245
+ try {
246
+ const existing = fs.readFileSync(customScssPath, "utf8");
247
+ if (!existing.includes("@nextbridgehq/payload-block-builder")) {
248
+ fs.appendFileSync(customScssPath, "\n" + CUSTOM_SCSS_IMPORTS);
249
+ console.log(`Updated: ${customScssPath} (added admin field styles)`);
250
+ } else {
251
+ console.log(`Skipped: ${customScssPath} already has block-builder imports`);
252
+ }
253
+ } catch (err) {
254
+ console.error(`Error: Could not update ${customScssPath}: ${err.message}`);
255
+ process.exit(1);
202
256
  }
203
257
  }
204
258
  const configPath = findPayloadConfig();
package/dist/client.cjs CHANGED
@@ -77,6 +77,56 @@ function MediaPicker({ label, required, value, onChange }) {
77
77
  "x"
78
78
  ))) : /* @__PURE__ */ import_react.default.createElement(ListDrawerToggler, { className: "bdf-upload-btn" }, "Choose from Media Library")), /* @__PURE__ */ import_react.default.createElement(ListDrawer, { onSelect: handleSelect }));
79
79
  }
80
+ function RelationshipPicker({ label, required, collection, value, onChange }) {
81
+ const changeRef = (0, import_react.useRef)(onChange);
82
+ const closeRef = (0, import_react.useRef)(() => {
83
+ });
84
+ (0, import_react.useEffect)(() => {
85
+ changeRef.current = onChange;
86
+ });
87
+ const handleSelect = (0, import_react.useCallback)(
88
+ ({ docID, doc }) => {
89
+ changeRef.current({
90
+ id: docID,
91
+ title: doc?.title ?? doc?.name ?? doc?.slug ?? null
92
+ });
93
+ closeRef.current();
94
+ },
95
+ []
96
+ );
97
+ const [ListDrawer, ListDrawerToggler, { closeDrawer }] = (0, import_ui.useListDrawer)({
98
+ collectionSlugs: [collection]
99
+ });
100
+ closeRef.current = closeDrawer;
101
+ const relObj = value && typeof value === "object" ? value : null;
102
+ const relId = relObj?.id ?? (typeof value === "string" || typeof value === "number" ? value : null);
103
+ const relTitle = relObj?.title ? String(relObj.title) : null;
104
+ const [fetchedTitle, setFetchedTitle] = (0, import_react.useState)(null);
105
+ (0, import_react.useEffect)(() => {
106
+ if (!relId || relTitle) {
107
+ setFetchedTitle(null);
108
+ return;
109
+ }
110
+ fetch(`/api/${collection}/${String(relId)}?depth=0`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : null).then((doc) => {
111
+ if (doc) {
112
+ const t = doc.title ?? doc.name ?? doc.slug ?? null;
113
+ setFetchedTitle(t ? String(t) : null);
114
+ }
115
+ }).catch(() => {
116
+ });
117
+ }, [relId, relTitle, collection]);
118
+ const displayTitle = relTitle ?? fetchedTitle;
119
+ return /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ import_react.default.createElement("label", { className: "bdf-label" }, label, required && /* @__PURE__ */ import_react.default.createElement("span", { className: "bdf-required" }, "*"), /* @__PURE__ */ import_react.default.createElement("span", { style: { marginLeft: 6, fontSize: 11, color: "var(--theme-elevation-400)", fontWeight: 400 } }, "(", collection, ")")), /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-upload-area" }, relId ? /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-upload-selected" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "bdf-upload-name" }, displayTitle ?? `ID: ${String(relId)}`), /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-upload-actions" }, /* @__PURE__ */ import_react.default.createElement(ListDrawerToggler, { className: "bdf-upload-btn" }, "Change"), /* @__PURE__ */ import_react.default.createElement(
120
+ "button",
121
+ {
122
+ type: "button",
123
+ className: "bdf-icon-btn bdf-icon-btn--danger",
124
+ title: "Remove",
125
+ onClick: () => onChange(null)
126
+ },
127
+ "\xD7"
128
+ ))) : /* @__PURE__ */ import_react.default.createElement(ListDrawerToggler, { className: "bdf-upload-btn" }, "Choose from ", collection)), /* @__PURE__ */ import_react.default.createElement(ListDrawer, { onSelect: handleSelect }));
129
+ }
80
130
  function SchemaForm({ schema, value, onChange }) {
81
131
  const set = (0, import_react.useCallback)(
82
132
  (key, val) => onChange({ ...value, [key]: val }),
@@ -214,17 +264,19 @@ function FieldInput({ field, value, onChange }) {
214
264
  onChange
215
265
  }
216
266
  );
217
- case "relationship":
218
- return /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ import_react.default.createElement("label", { className: "bdf-label" }, label, field.required && /* @__PURE__ */ import_react.default.createElement("span", { className: "bdf-required" }, "*"), field.collection && /* @__PURE__ */ import_react.default.createElement("span", { style: { marginLeft: 6, fontSize: 11, color: "var(--theme-elevation-400)", fontWeight: 400 } }, "(", field.collection, ")")), /* @__PURE__ */ import_react.default.createElement(
219
- "input",
267
+ case "relationship": {
268
+ const collection = field.collection ?? "media";
269
+ return /* @__PURE__ */ import_react.default.createElement(
270
+ RelationshipPicker,
220
271
  {
221
- className: "bdf-input",
222
- type: "text",
223
- value: value ?? "",
224
- onChange: (e) => onChange(e.target.value),
225
- placeholder: "Enter document ID"
272
+ label,
273
+ required: field.required,
274
+ collection,
275
+ value,
276
+ onChange
226
277
  }
227
- ));
278
+ );
279
+ }
228
280
  case "json":
229
281
  return /* @__PURE__ */ import_react.default.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ import_react.default.createElement("label", { className: "bdf-label" }, label, field.required && /* @__PURE__ */ import_react.default.createElement("span", { className: "bdf-required" }, "*"), /* @__PURE__ */ import_react.default.createElement("span", { style: { marginLeft: 6, fontSize: 11, color: "var(--theme-elevation-400)", fontWeight: 400 } }, "(JSON)")), /* @__PURE__ */ import_react.default.createElement(
230
282
  "textarea",
@@ -1172,6 +1224,7 @@ var useBuilderStore = (0, import_zustand.create)()(
1172
1224
 
1173
1225
  // src/block-builder/components/canvas/TopBar.tsx
1174
1226
  var import_react7 = __toESM(require("react"), 1);
1227
+ var import_lucide_react = require("lucide-react");
1175
1228
 
1176
1229
  // src/block-builder/lib/mapToSaveRequest.ts
1177
1230
  var TYPE_MAP = {
@@ -1360,7 +1413,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1360
1413
  return () => document.removeEventListener("mousedown", handleClick);
1361
1414
  }, []);
1362
1415
  async function handlePublish() {
1363
- if (!activeBlock || isReadOnly) return;
1416
+ if (!activeBlock || isReadOnly) return false;
1364
1417
  setNotification({ status: "publishing" });
1365
1418
  try {
1366
1419
  const req = mapToSaveRequest(activeBlock);
@@ -1377,13 +1430,15 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1377
1430
  status: "success",
1378
1431
  msg: `v${json.versionNumber ?? "?"} published successfully!`
1379
1432
  });
1380
- await onAfterPublish();
1433
+ onAfterPublish();
1434
+ return true;
1381
1435
  } else {
1382
1436
  setNotification({
1383
1437
  status: "error",
1384
1438
  title: "Failed to publish block",
1385
1439
  errors: json.errors ?? ["An unknown error occurred."]
1386
1440
  });
1441
+ return false;
1387
1442
  }
1388
1443
  } catch (err) {
1389
1444
  setNotification({
@@ -1391,6 +1446,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1391
1446
  title: "Network error",
1392
1447
  errors: [err instanceof Error ? err.message : "Could not reach the server."]
1393
1448
  });
1449
+ return false;
1394
1450
  }
1395
1451
  }
1396
1452
  function handleExport() {
@@ -1418,9 +1474,9 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1418
1474
  className: "bb-block-picker__trigger",
1419
1475
  onClick: () => setBlockPickerOpen((o) => !o)
1420
1476
  },
1421
- /* @__PURE__ */ import_react7.default.createElement("span", { className: "bb-block-picker__icon" }, "B"),
1477
+ /* @__PURE__ */ import_react7.default.createElement(import_lucide_react.Blocks, { size: 14, strokeWidth: 1.75, className: "bb-block-picker__icon" }),
1422
1478
  /* @__PURE__ */ import_react7.default.createElement("span", null, activeBlockDef?.name ?? activeSlug ?? "Select a block"),
1423
- /* @__PURE__ */ import_react7.default.createElement("span", { className: "bb-version-selector__chevron" }, "v")
1479
+ /* @__PURE__ */ import_react7.default.createElement(import_lucide_react.ChevronDown, { size: 14, strokeWidth: 1.75, className: "bb-version-selector__chevron" })
1424
1480
  ), blockPickerOpen && /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-block-picker__dropdown" }, /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-version-dropdown__header" }, "Block Definitions"), blockDefs.map((b) => /* @__PURE__ */ import_react7.default.createElement(
1425
1481
  "button",
1426
1482
  {
@@ -1444,7 +1500,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1444
1500
  /* @__PURE__ */ import_react7.default.createElement("span", { className: `bb-version-selector__dot${selectedVersion?.isCurrent ? " bb-version-selector__dot--current" : " bb-version-selector__dot--old"}` }),
1445
1501
  /* @__PURE__ */ import_react7.default.createElement("span", null, selectedVersion?.label ?? `v${selectedVersion?.versionNumber ?? "?"}`),
1446
1502
  selectedVersion?.isCurrent && /* @__PURE__ */ import_react7.default.createElement("span", { className: "bb-version-selector__badge" }, "current"),
1447
- /* @__PURE__ */ import_react7.default.createElement("span", { className: "bb-version-selector__chevron" }, "v")
1503
+ /* @__PURE__ */ import_react7.default.createElement(import_lucide_react.ChevronDown, { size: 14, strokeWidth: 1.75, className: "bb-version-selector__chevron" })
1448
1504
  ), versionDropdownOpen && /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-version-dropdown" }, /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-version-dropdown__header" }, "Version History"), versions.map((v) => /* @__PURE__ */ import_react7.default.createElement(
1449
1505
  "button",
1450
1506
  {
@@ -1485,9 +1541,8 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1485
1541
  {
1486
1542
  type: "button",
1487
1543
  onClick: async () => {
1488
- await handlePublish();
1489
- setVersionMeta(null, false);
1490
- onRestoreVersion();
1544
+ const success = await handlePublish();
1545
+ if (success) onRestoreVersion();
1491
1546
  },
1492
1547
  disabled: notification?.status === "publishing" || !activeBlock,
1493
1548
  className: "bb-btn bb-btn--warning"
@@ -1502,11 +1557,12 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1502
1557
  className: "bb-btn bb-btn--primary"
1503
1558
  },
1504
1559
  notification?.status === "publishing" ? "Publishing..." : "Publish to Payload"
1505
- ))), notification?.status === "publishing" && /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify bb-notify--publishing" }, /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__spinner" }), /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__title" }, isReadOnly ? "Restoring version..." : "Publishing to Payload..."), /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__sub" }, "Validating schema and saving block definition.")))), notification?.status === "success" && /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify bb-notify--success" }, /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ import_react7.default.createElement("span", { className: "bb-notify__icon" }, "OK"), /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__title" }, notification.msg), /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__sub" }, "The block definition and version have been saved.")), /* @__PURE__ */ import_react7.default.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, "x"))), notification?.status === "error" && /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify bb-notify--error" }, /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ import_react7.default.createElement("span", { className: "bb-notify__icon" }, "!"), /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__title" }, notification.title), /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__sub" }, "Fix the following errors before publishing:"), /* @__PURE__ */ import_react7.default.createElement("ul", { className: "bb-notify__error-list" }, notification.errors.map((e, i) => /* @__PURE__ */ import_react7.default.createElement("li", { key: i }, e)))), /* @__PURE__ */ import_react7.default.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, "x"))));
1560
+ ))), notification?.status === "publishing" && /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify bb-notify--publishing" }, /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__spinner" }), /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__title" }, isReadOnly ? "Restoring version..." : "Publishing to Payload..."), /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__sub" }, "Validating schema and saving block definition.")))), notification?.status === "success" && /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify bb-notify--success" }, /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ import_react7.default.createElement("span", { className: "bb-notify__icon" }, "OK"), /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__title" }, notification.msg), /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__sub" }, "The block definition and version have been saved.")), /* @__PURE__ */ import_react7.default.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, /* @__PURE__ */ import_react7.default.createElement(import_lucide_react.X, { size: 12, strokeWidth: 2 })))), notification?.status === "error" && /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify bb-notify--error" }, /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ import_react7.default.createElement("span", { className: "bb-notify__icon" }, "!"), /* @__PURE__ */ import_react7.default.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__title" }, notification.title), /* @__PURE__ */ import_react7.default.createElement("p", { className: "bb-notify__sub" }, "Fix the following errors before publishing:"), /* @__PURE__ */ import_react7.default.createElement("ul", { className: "bb-notify__error-list" }, notification.errors.map((e, i) => /* @__PURE__ */ import_react7.default.createElement("li", { key: i }, e)))), /* @__PURE__ */ import_react7.default.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, /* @__PURE__ */ import_react7.default.createElement(import_lucide_react.X, { size: 12, strokeWidth: 2 })))));
1506
1561
  }
1507
1562
 
1508
1563
  // src/block-builder/components/canvas/BlockList.tsx
1509
1564
  var import_react8 = __toESM(require("react"), 1);
1565
+ var import_lucide_react2 = require("lucide-react");
1510
1566
  function BlockList() {
1511
1567
  const blocks = useBuilderStore((s) => s.blocks);
1512
1568
  const activeBlockId = useBuilderStore((s) => s.activeBlockId);
@@ -1514,7 +1570,7 @@ function BlockList() {
1514
1570
  const removeBlock = useBuilderStore((s) => s.removeBlock);
1515
1571
  const duplicateBlock = useBuilderStore((s) => s.duplicateBlock);
1516
1572
  const setActiveBlock = useBuilderStore((s) => s.setActiveBlock);
1517
- return /* @__PURE__ */ import_react8.default.createElement("div", { className: "bb-sidebar bb-sidebar--200 bb-sidebar--blocks" }, /* @__PURE__ */ import_react8.default.createElement("div", { className: "bb-sidebar__header" }, /* @__PURE__ */ import_react8.default.createElement("span", { className: "bb-sidebar__title" }, "Blocks"), /* @__PURE__ */ import_react8.default.createElement("button", { type: "button", onClick: addBlock, title: "Add block", className: "bb-sidebar__add" }, "+")), /* @__PURE__ */ import_react8.default.createElement("div", { className: "bb-sidebar__body" }, blocks.length === 0 && /* @__PURE__ */ import_react8.default.createElement("div", { className: "bb-block-empty" }, "No blocks yet.", /* @__PURE__ */ import_react8.default.createElement("br", null), "Click + to create one."), blocks.map((block) => {
1573
+ return /* @__PURE__ */ import_react8.default.createElement("div", { className: "bb-sidebar bb-sidebar--200 bb-sidebar--blocks" }, /* @__PURE__ */ import_react8.default.createElement("div", { className: "bb-sidebar__header" }, /* @__PURE__ */ import_react8.default.createElement("span", { className: "bb-sidebar__title" }, "Blocks"), /* @__PURE__ */ import_react8.default.createElement("button", { type: "button", onClick: addBlock, title: "Add block", className: "bb-sidebar__add" }, /* @__PURE__ */ import_react8.default.createElement(import_lucide_react2.Plus, { size: 14, strokeWidth: 2 }))), /* @__PURE__ */ import_react8.default.createElement("div", { className: "bb-sidebar__body" }, blocks.length === 0 && /* @__PURE__ */ import_react8.default.createElement("div", { className: "bb-block-empty" }, "No blocks yet.", /* @__PURE__ */ import_react8.default.createElement("br", null), "Click + to create one."), blocks.map((block) => {
1518
1574
  const isActive = block.id === activeBlockId;
1519
1575
  return /* @__PURE__ */ import_react8.default.createElement(
1520
1576
  "div",
@@ -1539,7 +1595,7 @@ function BlockList() {
1539
1595
  className: "bb-block-action",
1540
1596
  title: "Duplicate"
1541
1597
  },
1542
- "="
1598
+ /* @__PURE__ */ import_react8.default.createElement(import_lucide_react2.Copy, { size: 12, strokeWidth: 1.75 })
1543
1599
  ),
1544
1600
  /* @__PURE__ */ import_react8.default.createElement(
1545
1601
  "button",
@@ -1549,7 +1605,7 @@ function BlockList() {
1549
1605
  className: "bb-block-action bb-block-action--danger",
1550
1606
  title: "Delete"
1551
1607
  },
1552
- "x"
1608
+ /* @__PURE__ */ import_react8.default.createElement(import_lucide_react2.Trash2, { size: 12, strokeWidth: 1.75 })
1553
1609
  )
1554
1610
  )
1555
1611
  );
@@ -1566,24 +1622,19 @@ var import_modifiers = require("@dnd-kit/modifiers");
1566
1622
  var import_react9 = __toESM(require("react"), 1);
1567
1623
  var import_sortable = require("@dnd-kit/sortable");
1568
1624
  var import_utilities = require("@dnd-kit/utilities");
1625
+ var import_lucide_react3 = require("lucide-react");
1569
1626
  var ICON_MAP = {
1570
- text: "T",
1571
- textarea: "Tx",
1572
- richText: "RT",
1573
- number: "#",
1574
- checkbox: "[x]",
1575
- select: "v",
1576
- radio: "(o)",
1577
- date: "D",
1578
- upload: "^",
1579
- email: "@",
1580
- code: "<>",
1581
- point: "P",
1582
- relationship: "->>",
1583
- array: "[]",
1584
- group: "{ }",
1585
- json: "{ }",
1586
- ui: "UI"
1627
+ text: import_lucide_react3.Type,
1628
+ textarea: import_lucide_react3.AlignLeft,
1629
+ number: import_lucide_react3.Hash,
1630
+ email: import_lucide_react3.Mail,
1631
+ date: import_lucide_react3.Calendar,
1632
+ checkbox: import_lucide_react3.CheckSquare,
1633
+ select: import_lucide_react3.ChevronDown,
1634
+ radio: import_lucide_react3.Circle,
1635
+ upload: import_lucide_react3.Upload,
1636
+ relationship: import_lucide_react3.Link,
1637
+ json: import_lucide_react3.Braces
1587
1638
  };
1588
1639
  function SortableFieldCard({ field, blockId, index }) {
1589
1640
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = (0, import_sortable.useSortable)({
@@ -1592,6 +1643,7 @@ function SortableFieldCard({ field, blockId, index }) {
1592
1643
  const activeFieldId = useBuilderStore((s) => s.activeFieldId);
1593
1644
  const setActiveField = useBuilderStore((s) => s.setActiveField);
1594
1645
  const removeField = useBuilderStore((s) => s.removeField);
1646
+ const isReadOnly = useBuilderStore((s) => s.isReadOnly);
1595
1647
  const isActive = activeFieldId === field.id;
1596
1648
  const wrapStyle = {
1597
1649
  transform: import_utilities.CSS.Transform.toString(transform),
@@ -1612,10 +1664,10 @@ function SortableFieldCard({ field, blockId, index }) {
1612
1664
  className: `bb-field-card${isActive ? " bb-field-card--active" : ""}`,
1613
1665
  onClick: () => setActiveField(isActive ? null : field.id)
1614
1666
  },
1615
- /* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-field-card__icon" }, ICON_MAP[field.type] ?? "?"),
1667
+ /* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-field-card__icon" }, /* @__PURE__ */ import_react9.default.createElement(FieldIcon, { type: field.type })),
1616
1668
  /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-field-card__body" }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-field-card__name" }, field.name || /* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-field-card__name--empty" }, "unnamed")), /* @__PURE__ */ import_react9.default.createElement("div", { className: "bb-field-card__type" }, field.type, field.required && /* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-field-card__required" }, "*"))),
1617
1669
  /* @__PURE__ */ import_react9.default.createElement("span", { className: "bb-field-card__index" }, "#", index + 1),
1618
- /* @__PURE__ */ import_react9.default.createElement(
1670
+ !isReadOnly && /* @__PURE__ */ import_react9.default.createElement(
1619
1671
  "button",
1620
1672
  {
1621
1673
  type: "button",
@@ -1627,11 +1679,15 @@ function SortableFieldCard({ field, blockId, index }) {
1627
1679
  className: "bb-field-card__delete",
1628
1680
  title: "Remove field"
1629
1681
  },
1630
- "x"
1682
+ /* @__PURE__ */ import_react9.default.createElement(import_lucide_react3.X, { size: 12, strokeWidth: 2 })
1631
1683
  )
1632
1684
  )
1633
1685
  );
1634
1686
  }
1687
+ function FieldIcon({ type }) {
1688
+ const Icon = ICON_MAP[type];
1689
+ return Icon ? /* @__PURE__ */ import_react9.default.createElement(Icon, { size: 13, strokeWidth: 1.75 }) : null;
1690
+ }
1635
1691
 
1636
1692
  // src/block-builder/components/canvas/BuilderCanvas.tsx
1637
1693
  function BuilderCanvas() {
@@ -1748,21 +1804,15 @@ var import_react12 = __toESM(require("react"), 1);
1748
1804
  var ALL_TYPES = [
1749
1805
  "text",
1750
1806
  "textarea",
1751
- "richText",
1752
1807
  "number",
1808
+ "email",
1809
+ "date",
1753
1810
  "checkbox",
1754
1811
  "select",
1755
1812
  "radio",
1756
- "date",
1757
1813
  "upload",
1758
- "email",
1759
- "code",
1760
- "point",
1761
1814
  "relationship",
1762
- "array",
1763
- "group",
1764
- "json",
1765
- "ui"
1815
+ "json"
1766
1816
  ];
1767
1817
  function FieldConfig() {
1768
1818
  const activeBlockId = useBuilderStore((s) => s.activeBlockId);
@@ -1934,7 +1984,7 @@ function ConfigPanel() {
1934
1984
 
1935
1985
  // src/block-builder/components/sidebar/FieldPalette.tsx
1936
1986
  var import_react14 = __toESM(require("react"), 1);
1937
- var import_lucide_react = require("lucide-react");
1987
+ var import_lucide_react4 = require("lucide-react");
1938
1988
 
1939
1989
  // src/block-builder/lib/field-palette.ts
1940
1990
  var FIELD_PALETTE = [
@@ -1968,19 +2018,20 @@ function getFieldMeta(type) {
1968
2018
 
1969
2019
  // src/block-builder/components/sidebar/FieldPalette.tsx
1970
2020
  var ICON_MAP2 = {
1971
- Type: import_lucide_react.Type,
1972
- AlignLeft: import_lucide_react.AlignLeft,
1973
- Hash: import_lucide_react.Hash,
1974
- Mail: import_lucide_react.Mail,
1975
- Calendar: import_lucide_react.Calendar,
1976
- CheckSquare: import_lucide_react.CheckSquare,
1977
- ChevronDown: import_lucide_react.ChevronDown,
1978
- Circle: import_lucide_react.Circle,
1979
- Upload: import_lucide_react.Upload,
1980
- Link: import_lucide_react.Link,
1981
- List: import_lucide_react.List,
1982
- Folder: import_lucide_react.Folder,
1983
- Braces: import_lucide_react.Braces
2021
+ Type: import_lucide_react4.Type,
2022
+ AlignLeft: import_lucide_react4.AlignLeft,
2023
+ AlignJustify: import_lucide_react4.AlignJustify,
2024
+ Hash: import_lucide_react4.Hash,
2025
+ Mail: import_lucide_react4.Mail,
2026
+ Calendar: import_lucide_react4.Calendar,
2027
+ CheckSquare: import_lucide_react4.CheckSquare,
2028
+ ChevronDown: import_lucide_react4.ChevronDown,
2029
+ Circle: import_lucide_react4.Circle,
2030
+ Upload: import_lucide_react4.Upload,
2031
+ Link: import_lucide_react4.Link,
2032
+ List: import_lucide_react4.List,
2033
+ Folder: import_lucide_react4.Folder,
2034
+ Braces: import_lucide_react4.Braces
1984
2035
  };
1985
2036
  function FieldPalette() {
1986
2037
  const [search, setSearch] = (0, import_react14.useState)("");
@@ -2333,7 +2384,9 @@ function BuilderShell({ loadSlug }) {
2333
2384
  setBlockDefs(
2334
2385
  (json.docs ?? []).map((d) => ({ id: String(d.id), slug: d.slug, name: d.name }))
2335
2386
  );
2336
- }).catch(() => {
2387
+ }).catch((err) => {
2388
+ console.error("[block-builder] Failed to load block definitions:", err);
2389
+ setLoadError("Could not load block definitions. Please refresh the page.");
2337
2390
  });
2338
2391
  }, []);
2339
2392
  const loadVersionsForSlug = (0, import_react16.useCallback)(async (slug) => {
package/dist/client.js CHANGED
@@ -39,6 +39,56 @@ function MediaPicker({ label, required, value, onChange }) {
39
39
  "x"
40
40
  ))) : /* @__PURE__ */ React.createElement(ListDrawerToggler, { className: "bdf-upload-btn" }, "Choose from Media Library")), /* @__PURE__ */ React.createElement(ListDrawer, { onSelect: handleSelect }));
41
41
  }
42
+ function RelationshipPicker({ label, required, collection, value, onChange }) {
43
+ const changeRef = useRef(onChange);
44
+ const closeRef = useRef(() => {
45
+ });
46
+ useEffect(() => {
47
+ changeRef.current = onChange;
48
+ });
49
+ const handleSelect = useCallback(
50
+ ({ docID, doc }) => {
51
+ changeRef.current({
52
+ id: docID,
53
+ title: doc?.title ?? doc?.name ?? doc?.slug ?? null
54
+ });
55
+ closeRef.current();
56
+ },
57
+ []
58
+ );
59
+ const [ListDrawer, ListDrawerToggler, { closeDrawer }] = useListDrawer({
60
+ collectionSlugs: [collection]
61
+ });
62
+ closeRef.current = closeDrawer;
63
+ const relObj = value && typeof value === "object" ? value : null;
64
+ const relId = relObj?.id ?? (typeof value === "string" || typeof value === "number" ? value : null);
65
+ const relTitle = relObj?.title ? String(relObj.title) : null;
66
+ const [fetchedTitle, setFetchedTitle] = useState(null);
67
+ useEffect(() => {
68
+ if (!relId || relTitle) {
69
+ setFetchedTitle(null);
70
+ return;
71
+ }
72
+ fetch(`/api/${collection}/${String(relId)}?depth=0`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : null).then((doc) => {
73
+ if (doc) {
74
+ const t = doc.title ?? doc.name ?? doc.slug ?? null;
75
+ setFetchedTitle(t ? String(t) : null);
76
+ }
77
+ }).catch(() => {
78
+ });
79
+ }, [relId, relTitle, collection]);
80
+ const displayTitle = relTitle ?? fetchedTitle;
81
+ return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("label", { className: "bdf-label" }, label, required && /* @__PURE__ */ React.createElement("span", { className: "bdf-required" }, "*"), /* @__PURE__ */ React.createElement("span", { style: { marginLeft: 6, fontSize: 11, color: "var(--theme-elevation-400)", fontWeight: 400 } }, "(", collection, ")")), /* @__PURE__ */ React.createElement("div", { className: "bdf-upload-area" }, relId ? /* @__PURE__ */ React.createElement("div", { className: "bdf-upload-selected" }, /* @__PURE__ */ React.createElement("span", { className: "bdf-upload-name" }, displayTitle ?? `ID: ${String(relId)}`), /* @__PURE__ */ React.createElement("div", { className: "bdf-upload-actions" }, /* @__PURE__ */ React.createElement(ListDrawerToggler, { className: "bdf-upload-btn" }, "Change"), /* @__PURE__ */ React.createElement(
82
+ "button",
83
+ {
84
+ type: "button",
85
+ className: "bdf-icon-btn bdf-icon-btn--danger",
86
+ title: "Remove",
87
+ onClick: () => onChange(null)
88
+ },
89
+ "\xD7"
90
+ ))) : /* @__PURE__ */ React.createElement(ListDrawerToggler, { className: "bdf-upload-btn" }, "Choose from ", collection)), /* @__PURE__ */ React.createElement(ListDrawer, { onSelect: handleSelect }));
91
+ }
42
92
  function SchemaForm({ schema, value, onChange }) {
43
93
  const set = useCallback(
44
94
  (key, val) => onChange({ ...value, [key]: val }),
@@ -176,17 +226,19 @@ function FieldInput({ field, value, onChange }) {
176
226
  onChange
177
227
  }
178
228
  );
179
- case "relationship":
180
- return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("label", { className: "bdf-label" }, label, field.required && /* @__PURE__ */ React.createElement("span", { className: "bdf-required" }, "*"), field.collection && /* @__PURE__ */ React.createElement("span", { style: { marginLeft: 6, fontSize: 11, color: "var(--theme-elevation-400)", fontWeight: 400 } }, "(", field.collection, ")")), /* @__PURE__ */ React.createElement(
181
- "input",
229
+ case "relationship": {
230
+ const collection = field.collection ?? "media";
231
+ return /* @__PURE__ */ React.createElement(
232
+ RelationshipPicker,
182
233
  {
183
- className: "bdf-input",
184
- type: "text",
185
- value: value ?? "",
186
- onChange: (e) => onChange(e.target.value),
187
- placeholder: "Enter document ID"
234
+ label,
235
+ required: field.required,
236
+ collection,
237
+ value,
238
+ onChange
188
239
  }
189
- ));
240
+ );
241
+ }
190
242
  case "json":
191
243
  return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("label", { className: "bdf-label" }, label, field.required && /* @__PURE__ */ React.createElement("span", { className: "bdf-required" }, "*"), /* @__PURE__ */ React.createElement("span", { style: { marginLeft: 6, fontSize: 11, color: "var(--theme-elevation-400)", fontWeight: 400 } }, "(JSON)")), /* @__PURE__ */ React.createElement(
192
244
  "textarea",
@@ -1134,6 +1186,7 @@ var useBuilderStore = create()(
1134
1186
 
1135
1187
  // src/block-builder/components/canvas/TopBar.tsx
1136
1188
  import React7, { useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
1189
+ import { Blocks, ChevronDown, X } from "lucide-react";
1137
1190
 
1138
1191
  // src/block-builder/lib/mapToSaveRequest.ts
1139
1192
  var TYPE_MAP = {
@@ -1322,7 +1375,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1322
1375
  return () => document.removeEventListener("mousedown", handleClick);
1323
1376
  }, []);
1324
1377
  async function handlePublish() {
1325
- if (!activeBlock || isReadOnly) return;
1378
+ if (!activeBlock || isReadOnly) return false;
1326
1379
  setNotification({ status: "publishing" });
1327
1380
  try {
1328
1381
  const req = mapToSaveRequest(activeBlock);
@@ -1339,13 +1392,15 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1339
1392
  status: "success",
1340
1393
  msg: `v${json.versionNumber ?? "?"} published successfully!`
1341
1394
  });
1342
- await onAfterPublish();
1395
+ onAfterPublish();
1396
+ return true;
1343
1397
  } else {
1344
1398
  setNotification({
1345
1399
  status: "error",
1346
1400
  title: "Failed to publish block",
1347
1401
  errors: json.errors ?? ["An unknown error occurred."]
1348
1402
  });
1403
+ return false;
1349
1404
  }
1350
1405
  } catch (err) {
1351
1406
  setNotification({
@@ -1353,6 +1408,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1353
1408
  title: "Network error",
1354
1409
  errors: [err instanceof Error ? err.message : "Could not reach the server."]
1355
1410
  });
1411
+ return false;
1356
1412
  }
1357
1413
  }
1358
1414
  function handleExport() {
@@ -1380,9 +1436,9 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1380
1436
  className: "bb-block-picker__trigger",
1381
1437
  onClick: () => setBlockPickerOpen((o) => !o)
1382
1438
  },
1383
- /* @__PURE__ */ React7.createElement("span", { className: "bb-block-picker__icon" }, "B"),
1439
+ /* @__PURE__ */ React7.createElement(Blocks, { size: 14, strokeWidth: 1.75, className: "bb-block-picker__icon" }),
1384
1440
  /* @__PURE__ */ React7.createElement("span", null, activeBlockDef?.name ?? activeSlug ?? "Select a block"),
1385
- /* @__PURE__ */ React7.createElement("span", { className: "bb-version-selector__chevron" }, "v")
1441
+ /* @__PURE__ */ React7.createElement(ChevronDown, { size: 14, strokeWidth: 1.75, className: "bb-version-selector__chevron" })
1386
1442
  ), blockPickerOpen && /* @__PURE__ */ React7.createElement("div", { className: "bb-block-picker__dropdown" }, /* @__PURE__ */ React7.createElement("div", { className: "bb-version-dropdown__header" }, "Block Definitions"), blockDefs.map((b) => /* @__PURE__ */ React7.createElement(
1387
1443
  "button",
1388
1444
  {
@@ -1406,7 +1462,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1406
1462
  /* @__PURE__ */ React7.createElement("span", { className: `bb-version-selector__dot${selectedVersion?.isCurrent ? " bb-version-selector__dot--current" : " bb-version-selector__dot--old"}` }),
1407
1463
  /* @__PURE__ */ React7.createElement("span", null, selectedVersion?.label ?? `v${selectedVersion?.versionNumber ?? "?"}`),
1408
1464
  selectedVersion?.isCurrent && /* @__PURE__ */ React7.createElement("span", { className: "bb-version-selector__badge" }, "current"),
1409
- /* @__PURE__ */ React7.createElement("span", { className: "bb-version-selector__chevron" }, "v")
1465
+ /* @__PURE__ */ React7.createElement(ChevronDown, { size: 14, strokeWidth: 1.75, className: "bb-version-selector__chevron" })
1410
1466
  ), versionDropdownOpen && /* @__PURE__ */ React7.createElement("div", { className: "bb-version-dropdown" }, /* @__PURE__ */ React7.createElement("div", { className: "bb-version-dropdown__header" }, "Version History"), versions.map((v) => /* @__PURE__ */ React7.createElement(
1411
1467
  "button",
1412
1468
  {
@@ -1447,9 +1503,8 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1447
1503
  {
1448
1504
  type: "button",
1449
1505
  onClick: async () => {
1450
- await handlePublish();
1451
- setVersionMeta(null, false);
1452
- onRestoreVersion();
1506
+ const success = await handlePublish();
1507
+ if (success) onRestoreVersion();
1453
1508
  },
1454
1509
  disabled: notification?.status === "publishing" || !activeBlock,
1455
1510
  className: "bb-btn bb-btn--warning"
@@ -1464,11 +1519,12 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
1464
1519
  className: "bb-btn bb-btn--primary"
1465
1520
  },
1466
1521
  notification?.status === "publishing" ? "Publishing..." : "Publish to Payload"
1467
- ))), notification?.status === "publishing" && /* @__PURE__ */ React7.createElement("div", { className: "bb-notify bb-notify--publishing" }, /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__spinner" }), /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__title" }, isReadOnly ? "Restoring version..." : "Publishing to Payload..."), /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__sub" }, "Validating schema and saving block definition.")))), notification?.status === "success" && /* @__PURE__ */ React7.createElement("div", { className: "bb-notify bb-notify--success" }, /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ React7.createElement("span", { className: "bb-notify__icon" }, "OK"), /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__title" }, notification.msg), /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__sub" }, "The block definition and version have been saved.")), /* @__PURE__ */ React7.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, "x"))), notification?.status === "error" && /* @__PURE__ */ React7.createElement("div", { className: "bb-notify bb-notify--error" }, /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ React7.createElement("span", { className: "bb-notify__icon" }, "!"), /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__title" }, notification.title), /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__sub" }, "Fix the following errors before publishing:"), /* @__PURE__ */ React7.createElement("ul", { className: "bb-notify__error-list" }, notification.errors.map((e, i) => /* @__PURE__ */ React7.createElement("li", { key: i }, e)))), /* @__PURE__ */ React7.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, "x"))));
1522
+ ))), notification?.status === "publishing" && /* @__PURE__ */ React7.createElement("div", { className: "bb-notify bb-notify--publishing" }, /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__spinner" }), /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__title" }, isReadOnly ? "Restoring version..." : "Publishing to Payload..."), /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__sub" }, "Validating schema and saving block definition.")))), notification?.status === "success" && /* @__PURE__ */ React7.createElement("div", { className: "bb-notify bb-notify--success" }, /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ React7.createElement("span", { className: "bb-notify__icon" }, "OK"), /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__title" }, notification.msg), /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__sub" }, "The block definition and version have been saved.")), /* @__PURE__ */ React7.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, /* @__PURE__ */ React7.createElement(X, { size: 12, strokeWidth: 2 })))), notification?.status === "error" && /* @__PURE__ */ React7.createElement("div", { className: "bb-notify bb-notify--error" }, /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ React7.createElement("span", { className: "bb-notify__icon" }, "!"), /* @__PURE__ */ React7.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__title" }, notification.title), /* @__PURE__ */ React7.createElement("p", { className: "bb-notify__sub" }, "Fix the following errors before publishing:"), /* @__PURE__ */ React7.createElement("ul", { className: "bb-notify__error-list" }, notification.errors.map((e, i) => /* @__PURE__ */ React7.createElement("li", { key: i }, e)))), /* @__PURE__ */ React7.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, /* @__PURE__ */ React7.createElement(X, { size: 12, strokeWidth: 2 })))));
1468
1523
  }
1469
1524
 
1470
1525
  // src/block-builder/components/canvas/BlockList.tsx
1471
1526
  import React8 from "react";
1527
+ import { Copy, Trash2, Plus } from "lucide-react";
1472
1528
  function BlockList() {
1473
1529
  const blocks = useBuilderStore((s) => s.blocks);
1474
1530
  const activeBlockId = useBuilderStore((s) => s.activeBlockId);
@@ -1476,7 +1532,7 @@ function BlockList() {
1476
1532
  const removeBlock = useBuilderStore((s) => s.removeBlock);
1477
1533
  const duplicateBlock = useBuilderStore((s) => s.duplicateBlock);
1478
1534
  const setActiveBlock = useBuilderStore((s) => s.setActiveBlock);
1479
- return /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar bb-sidebar--200 bb-sidebar--blocks" }, /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar__header" }, /* @__PURE__ */ React8.createElement("span", { className: "bb-sidebar__title" }, "Blocks"), /* @__PURE__ */ React8.createElement("button", { type: "button", onClick: addBlock, title: "Add block", className: "bb-sidebar__add" }, "+")), /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar__body" }, blocks.length === 0 && /* @__PURE__ */ React8.createElement("div", { className: "bb-block-empty" }, "No blocks yet.", /* @__PURE__ */ React8.createElement("br", null), "Click + to create one."), blocks.map((block) => {
1535
+ return /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar bb-sidebar--200 bb-sidebar--blocks" }, /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar__header" }, /* @__PURE__ */ React8.createElement("span", { className: "bb-sidebar__title" }, "Blocks"), /* @__PURE__ */ React8.createElement("button", { type: "button", onClick: addBlock, title: "Add block", className: "bb-sidebar__add" }, /* @__PURE__ */ React8.createElement(Plus, { size: 14, strokeWidth: 2 }))), /* @__PURE__ */ React8.createElement("div", { className: "bb-sidebar__body" }, blocks.length === 0 && /* @__PURE__ */ React8.createElement("div", { className: "bb-block-empty" }, "No blocks yet.", /* @__PURE__ */ React8.createElement("br", null), "Click + to create one."), blocks.map((block) => {
1480
1536
  const isActive = block.id === activeBlockId;
1481
1537
  return /* @__PURE__ */ React8.createElement(
1482
1538
  "div",
@@ -1501,7 +1557,7 @@ function BlockList() {
1501
1557
  className: "bb-block-action",
1502
1558
  title: "Duplicate"
1503
1559
  },
1504
- "="
1560
+ /* @__PURE__ */ React8.createElement(Copy, { size: 12, strokeWidth: 1.75 })
1505
1561
  ),
1506
1562
  /* @__PURE__ */ React8.createElement(
1507
1563
  "button",
@@ -1511,7 +1567,7 @@ function BlockList() {
1511
1567
  className: "bb-block-action bb-block-action--danger",
1512
1568
  title: "Delete"
1513
1569
  },
1514
- "x"
1570
+ /* @__PURE__ */ React8.createElement(Trash2, { size: 12, strokeWidth: 1.75 })
1515
1571
  )
1516
1572
  )
1517
1573
  );
@@ -1539,24 +1595,32 @@ import { restrictToVerticalAxis, restrictToParentElement } from "@dnd-kit/modifi
1539
1595
  import React9 from "react";
1540
1596
  import { useSortable } from "@dnd-kit/sortable";
1541
1597
  import { CSS } from "@dnd-kit/utilities";
1598
+ import {
1599
+ Type,
1600
+ AlignLeft,
1601
+ Hash,
1602
+ Mail,
1603
+ Calendar,
1604
+ CheckSquare,
1605
+ ChevronDown as ChevronDown2,
1606
+ Circle,
1607
+ Upload,
1608
+ Link,
1609
+ Braces,
1610
+ X as X2
1611
+ } from "lucide-react";
1542
1612
  var ICON_MAP = {
1543
- text: "T",
1544
- textarea: "Tx",
1545
- richText: "RT",
1546
- number: "#",
1547
- checkbox: "[x]",
1548
- select: "v",
1549
- radio: "(o)",
1550
- date: "D",
1551
- upload: "^",
1552
- email: "@",
1553
- code: "<>",
1554
- point: "P",
1555
- relationship: "->>",
1556
- array: "[]",
1557
- group: "{ }",
1558
- json: "{ }",
1559
- ui: "UI"
1613
+ text: Type,
1614
+ textarea: AlignLeft,
1615
+ number: Hash,
1616
+ email: Mail,
1617
+ date: Calendar,
1618
+ checkbox: CheckSquare,
1619
+ select: ChevronDown2,
1620
+ radio: Circle,
1621
+ upload: Upload,
1622
+ relationship: Link,
1623
+ json: Braces
1560
1624
  };
1561
1625
  function SortableFieldCard({ field, blockId, index }) {
1562
1626
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
@@ -1565,6 +1629,7 @@ function SortableFieldCard({ field, blockId, index }) {
1565
1629
  const activeFieldId = useBuilderStore((s) => s.activeFieldId);
1566
1630
  const setActiveField = useBuilderStore((s) => s.setActiveField);
1567
1631
  const removeField = useBuilderStore((s) => s.removeField);
1632
+ const isReadOnly = useBuilderStore((s) => s.isReadOnly);
1568
1633
  const isActive = activeFieldId === field.id;
1569
1634
  const wrapStyle = {
1570
1635
  transform: CSS.Transform.toString(transform),
@@ -1585,10 +1650,10 @@ function SortableFieldCard({ field, blockId, index }) {
1585
1650
  className: `bb-field-card${isActive ? " bb-field-card--active" : ""}`,
1586
1651
  onClick: () => setActiveField(isActive ? null : field.id)
1587
1652
  },
1588
- /* @__PURE__ */ React9.createElement("span", { className: "bb-field-card__icon" }, ICON_MAP[field.type] ?? "?"),
1653
+ /* @__PURE__ */ React9.createElement("span", { className: "bb-field-card__icon" }, /* @__PURE__ */ React9.createElement(FieldIcon, { type: field.type })),
1589
1654
  /* @__PURE__ */ React9.createElement("div", { className: "bb-field-card__body" }, /* @__PURE__ */ React9.createElement("div", { className: "bb-field-card__name" }, field.name || /* @__PURE__ */ React9.createElement("span", { className: "bb-field-card__name--empty" }, "unnamed")), /* @__PURE__ */ React9.createElement("div", { className: "bb-field-card__type" }, field.type, field.required && /* @__PURE__ */ React9.createElement("span", { className: "bb-field-card__required" }, "*"))),
1590
1655
  /* @__PURE__ */ React9.createElement("span", { className: "bb-field-card__index" }, "#", index + 1),
1591
- /* @__PURE__ */ React9.createElement(
1656
+ !isReadOnly && /* @__PURE__ */ React9.createElement(
1592
1657
  "button",
1593
1658
  {
1594
1659
  type: "button",
@@ -1600,11 +1665,15 @@ function SortableFieldCard({ field, blockId, index }) {
1600
1665
  className: "bb-field-card__delete",
1601
1666
  title: "Remove field"
1602
1667
  },
1603
- "x"
1668
+ /* @__PURE__ */ React9.createElement(X2, { size: 12, strokeWidth: 2 })
1604
1669
  )
1605
1670
  )
1606
1671
  );
1607
1672
  }
1673
+ function FieldIcon({ type }) {
1674
+ const Icon = ICON_MAP[type];
1675
+ return Icon ? /* @__PURE__ */ React9.createElement(Icon, { size: 13, strokeWidth: 1.75 }) : null;
1676
+ }
1608
1677
 
1609
1678
  // src/block-builder/components/canvas/BuilderCanvas.tsx
1610
1679
  function BuilderCanvas() {
@@ -1721,21 +1790,15 @@ import React12 from "react";
1721
1790
  var ALL_TYPES = [
1722
1791
  "text",
1723
1792
  "textarea",
1724
- "richText",
1725
1793
  "number",
1794
+ "email",
1795
+ "date",
1726
1796
  "checkbox",
1727
1797
  "select",
1728
1798
  "radio",
1729
- "date",
1730
1799
  "upload",
1731
- "email",
1732
- "code",
1733
- "point",
1734
1800
  "relationship",
1735
- "array",
1736
- "group",
1737
- "json",
1738
- "ui"
1801
+ "json"
1739
1802
  ];
1740
1803
  function FieldConfig() {
1741
1804
  const activeBlockId = useBuilderStore((s) => s.activeBlockId);
@@ -1908,19 +1971,20 @@ function ConfigPanel() {
1908
1971
  // src/block-builder/components/sidebar/FieldPalette.tsx
1909
1972
  import React14, { useState as useState6 } from "react";
1910
1973
  import {
1911
- Type,
1912
- AlignLeft,
1913
- Hash,
1914
- Mail,
1915
- Calendar,
1916
- CheckSquare,
1917
- ChevronDown,
1918
- Circle,
1919
- Upload,
1920
- Link,
1974
+ Type as Type2,
1975
+ AlignLeft as AlignLeft2,
1976
+ AlignJustify,
1977
+ Hash as Hash2,
1978
+ Mail as Mail2,
1979
+ Calendar as Calendar2,
1980
+ CheckSquare as CheckSquare2,
1981
+ ChevronDown as ChevronDown3,
1982
+ Circle as Circle2,
1983
+ Upload as Upload2,
1984
+ Link as Link2,
1921
1985
  List,
1922
1986
  Folder,
1923
- Braces
1987
+ Braces as Braces2
1924
1988
  } from "lucide-react";
1925
1989
 
1926
1990
  // src/block-builder/lib/field-palette.ts
@@ -1955,19 +2019,20 @@ function getFieldMeta(type) {
1955
2019
 
1956
2020
  // src/block-builder/components/sidebar/FieldPalette.tsx
1957
2021
  var ICON_MAP2 = {
1958
- Type,
1959
- AlignLeft,
1960
- Hash,
1961
- Mail,
1962
- Calendar,
1963
- CheckSquare,
1964
- ChevronDown,
1965
- Circle,
1966
- Upload,
1967
- Link,
2022
+ Type: Type2,
2023
+ AlignLeft: AlignLeft2,
2024
+ AlignJustify,
2025
+ Hash: Hash2,
2026
+ Mail: Mail2,
2027
+ Calendar: Calendar2,
2028
+ CheckSquare: CheckSquare2,
2029
+ ChevronDown: ChevronDown3,
2030
+ Circle: Circle2,
2031
+ Upload: Upload2,
2032
+ Link: Link2,
1968
2033
  List,
1969
2034
  Folder,
1970
- Braces
2035
+ Braces: Braces2
1971
2036
  };
1972
2037
  function FieldPalette() {
1973
2038
  const [search, setSearch] = useState6("");
@@ -2320,7 +2385,9 @@ function BuilderShell({ loadSlug }) {
2320
2385
  setBlockDefs(
2321
2386
  (json.docs ?? []).map((d) => ({ id: String(d.id), slug: d.slug, name: d.name }))
2322
2387
  );
2323
- }).catch(() => {
2388
+ }).catch((err) => {
2389
+ console.error("[block-builder] Failed to load block definitions:", err);
2390
+ setLoadError("Could not load block definitions. Please refresh the page.");
2324
2391
  });
2325
2392
  }, []);
2326
2393
  const loadVersionsForSlug = useCallback5(async (slug) => {
@@ -2437,10 +2504,10 @@ function BuilderShell({ loadSlug }) {
2437
2504
 
2438
2505
  // src/components/BlockBuilderNavLink/index.tsx
2439
2506
  import React17 from "react";
2440
- import Link2 from "next/link";
2507
+ import Link3 from "next/link";
2441
2508
  function BlockBuilderNavLink() {
2442
2509
  return /* @__PURE__ */ React17.createElement("div", { style: { padding: "0 16px", marginTop: "8px" } }, /* @__PURE__ */ React17.createElement(
2443
- Link2,
2510
+ Link3,
2444
2511
  {
2445
2512
  href: "/block-builder",
2446
2513
  target: "_blank",
package/dist/index.cjs CHANGED
@@ -157,18 +157,23 @@ function dbLayoutField(fieldName = "dbLayout", tabLabel = "DB Layout") {
157
157
  },
158
158
  fields: [
159
159
  {
160
- name: "blockDefinition",
161
- type: "relationship",
162
- relationTo: "block-definitions",
163
- required: true,
164
- admin: { description: "Which block type to use." }
165
- },
166
- {
167
- name: "blockVersion",
168
- type: "relationship",
169
- relationTo: "block-definition-versions",
170
- required: true,
171
- admin: { description: "Which schema version to use." }
160
+ type: "row",
161
+ fields: [
162
+ {
163
+ name: "blockDefinition",
164
+ type: "relationship",
165
+ relationTo: "block-definitions",
166
+ required: true,
167
+ admin: { description: "Which block type to use.", width: "50%" }
168
+ },
169
+ {
170
+ name: "blockVersion",
171
+ type: "relationship",
172
+ relationTo: "block-definition-versions",
173
+ required: true,
174
+ admin: { description: "Which schema version to use.", width: "50%" }
175
+ }
176
+ ]
172
177
  },
173
178
  {
174
179
  name: "instanceId",
@@ -360,7 +365,15 @@ var generateEndpoint = async (req) => {
360
365
  var import_uuid = require("uuid");
361
366
  var REVERSE_TYPE_MAP = {
362
367
  richtext: "richText",
363
- image: "upload"
368
+ image: "upload",
369
+ file: "upload",
370
+ // file and image both map to upload in the builder
371
+ multiselect: "select",
372
+ // builder has no multiselect — nearest equivalent
373
+ url: "text",
374
+ // builder has no url field — falls back to text
375
+ color: "text"
376
+ // builder has no color field — falls back to text
364
377
  };
365
378
  var VALID_BUILDER_TYPES = /* @__PURE__ */ new Set([
366
379
  "text",
@@ -387,7 +400,11 @@ var VALID_BUILDER_TYPES = /* @__PURE__ */ new Set([
387
400
  function fieldToBuilderField(raw) {
388
401
  const rawType = String(raw.type ?? "text");
389
402
  const mappedType = REVERSE_TYPE_MAP[rawType] ?? rawType;
390
- const fieldType = VALID_BUILDER_TYPES.has(mappedType) ? mappedType : "text";
403
+ const isKnown = VALID_BUILDER_TYPES.has(mappedType);
404
+ if (!isKnown) {
405
+ console.warn(`[block-builder] Unknown field type "${rawType}" \u2014 rendering as "text". Add a mapping in REVERSE_TYPE_MAP.`);
406
+ }
407
+ const fieldType = isKnown ? mappedType : "text";
391
408
  const field = {
392
409
  id: (0, import_uuid.v4)(),
393
410
  type: fieldType,
@@ -424,7 +441,7 @@ function fieldToBuilderField(raw) {
424
441
  function slugToInterfaceName(slug) {
425
442
  return slug.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
426
443
  }
427
- function schemaToBuilderBlock(slug, name, labels, schemaFields) {
444
+ function schemaToBuilderBlock(slug, _name, labels, schemaFields) {
428
445
  return {
429
446
  id: (0, import_uuid.v4)(),
430
447
  slug,
package/dist/index.js CHANGED
@@ -129,18 +129,23 @@ function dbLayoutField(fieldName = "dbLayout", tabLabel = "DB Layout") {
129
129
  },
130
130
  fields: [
131
131
  {
132
- name: "blockDefinition",
133
- type: "relationship",
134
- relationTo: "block-definitions",
135
- required: true,
136
- admin: { description: "Which block type to use." }
137
- },
138
- {
139
- name: "blockVersion",
140
- type: "relationship",
141
- relationTo: "block-definition-versions",
142
- required: true,
143
- admin: { description: "Which schema version to use." }
132
+ type: "row",
133
+ fields: [
134
+ {
135
+ name: "blockDefinition",
136
+ type: "relationship",
137
+ relationTo: "block-definitions",
138
+ required: true,
139
+ admin: { description: "Which block type to use.", width: "50%" }
140
+ },
141
+ {
142
+ name: "blockVersion",
143
+ type: "relationship",
144
+ relationTo: "block-definition-versions",
145
+ required: true,
146
+ admin: { description: "Which schema version to use.", width: "50%" }
147
+ }
148
+ ]
144
149
  },
145
150
  {
146
151
  name: "instanceId",
@@ -332,7 +337,15 @@ var generateEndpoint = async (req) => {
332
337
  import { v4 as uuidv4 } from "uuid";
333
338
  var REVERSE_TYPE_MAP = {
334
339
  richtext: "richText",
335
- image: "upload"
340
+ image: "upload",
341
+ file: "upload",
342
+ // file and image both map to upload in the builder
343
+ multiselect: "select",
344
+ // builder has no multiselect — nearest equivalent
345
+ url: "text",
346
+ // builder has no url field — falls back to text
347
+ color: "text"
348
+ // builder has no color field — falls back to text
336
349
  };
337
350
  var VALID_BUILDER_TYPES = /* @__PURE__ */ new Set([
338
351
  "text",
@@ -359,7 +372,11 @@ var VALID_BUILDER_TYPES = /* @__PURE__ */ new Set([
359
372
  function fieldToBuilderField(raw) {
360
373
  const rawType = String(raw.type ?? "text");
361
374
  const mappedType = REVERSE_TYPE_MAP[rawType] ?? rawType;
362
- const fieldType = VALID_BUILDER_TYPES.has(mappedType) ? mappedType : "text";
375
+ const isKnown = VALID_BUILDER_TYPES.has(mappedType);
376
+ if (!isKnown) {
377
+ console.warn(`[block-builder] Unknown field type "${rawType}" \u2014 rendering as "text". Add a mapping in REVERSE_TYPE_MAP.`);
378
+ }
379
+ const fieldType = isKnown ? mappedType : "text";
363
380
  const field = {
364
381
  id: uuidv4(),
365
382
  type: fieldType,
@@ -396,7 +413,7 @@ function fieldToBuilderField(raw) {
396
413
  function slugToInterfaceName(slug) {
397
414
  return slug.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
398
415
  }
399
- function schemaToBuilderBlock(slug, name, labels, schemaFields) {
416
+ function schemaToBuilderBlock(slug, _name, labels, schemaFields) {
400
417
  return {
401
418
  id: uuidv4(),
402
419
  slug,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@nextbridgehq/payload-block-builder",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Block Builder for Payload CMS",
5
- "keywords": ["payload", "payload-plugin", "cms", "block-builder", "dynamic-blocks"],
5
+ "keywords": ["payload", "payloadcms", "payload-plugin", "cms", "block-builder", "dynamic-blocks"],
6
6
  "homepage": "https://github.com/nextbridgehq/block-builder",
7
7
  "repository": {
8
8
  "type": "git",