@nextbridgehq/payload-block-builder 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -98
- package/dist/bin/init.js +0 -0
- package/dist/client.cjs +1251 -485
- package/dist/client.d.cts +2 -2
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1263 -472
- package/dist/index.cjs +874 -520
- package/dist/index.d.cts +52 -8
- package/dist/index.d.ts +52 -8
- package/dist/index.js +871 -518
- package/package.json +29 -10
- package/src/block-builder/builder.css +391 -36
- package/src/components/BlockDataField/BlockDataField.css +482 -393
- package/src/components/SchemaBuilderField/SchemaBuilderField.css +361 -361
package/dist/client.js
CHANGED
|
@@ -97,7 +97,8 @@ function RelationshipPicker({ label, required, collection, hasMany = false, valu
|
|
|
97
97
|
const t = doc.title ?? doc.name ?? doc.slug ?? null;
|
|
98
98
|
if (t) setFetchedTitles((prev) => ({ ...prev, [idStr]: String(t) }));
|
|
99
99
|
}
|
|
100
|
-
}).catch(() => {
|
|
100
|
+
}).catch((err) => {
|
|
101
|
+
console.error("[Block Builder] Failed to load relation title:", err);
|
|
101
102
|
fetchingRef.current.delete(idStr);
|
|
102
103
|
});
|
|
103
104
|
});
|
|
@@ -166,22 +167,79 @@ function JsonField({ label, required, value, onChange }) {
|
|
|
166
167
|
}
|
|
167
168
|
), hasError && /* @__PURE__ */ React.createElement("div", { className: "bdf-error", style: { marginTop: 4 } }, "Invalid JSON \u2014 changes not saved until fixed."));
|
|
168
169
|
}
|
|
169
|
-
function SchemaForm({ schema, value, onChange }) {
|
|
170
|
+
function SchemaForm({ schema, value, onChange, depth = 0 }) {
|
|
170
171
|
const set = useCallback(
|
|
171
172
|
(key, val) => onChange({ ...value, [key]: val }),
|
|
172
173
|
[value, onChange]
|
|
173
174
|
);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
onChange: (v) => set(field.name, v)
|
|
175
|
+
if (depth > 10) {
|
|
176
|
+
return /* @__PURE__ */ React.createElement("div", { className: "bdf-error" }, "Max nesting depth reached (10).");
|
|
177
|
+
}
|
|
178
|
+
return /* @__PURE__ */ React.createElement(React.Fragment, null, schema.map((field) => {
|
|
179
|
+
if (field.type === "row") {
|
|
180
|
+
return /* @__PURE__ */ React.createElement("div", { key: field.name, className: "bdf-row" }, /* @__PURE__ */ React.createElement(SchemaForm, { schema: field.fields, value, onChange, depth: depth + 1 }));
|
|
181
181
|
}
|
|
182
|
-
|
|
182
|
+
if (field.type === "collapsible") {
|
|
183
|
+
return /* @__PURE__ */ React.createElement(CollapsibleSection, { key: field.name, field, value, onChange, depth: depth + 1 });
|
|
184
|
+
}
|
|
185
|
+
if (field.type === "tabs") {
|
|
186
|
+
return /* @__PURE__ */ React.createElement(TabsSection, { key: field.name, field, value, onChange, depth: depth + 1 });
|
|
187
|
+
}
|
|
188
|
+
return /* @__PURE__ */ React.createElement(
|
|
189
|
+
FieldInput,
|
|
190
|
+
{
|
|
191
|
+
key: field.name,
|
|
192
|
+
field,
|
|
193
|
+
value: value[field.name],
|
|
194
|
+
onChange: (v) => set(field.name, v),
|
|
195
|
+
depth
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
function CollapsibleSection({
|
|
201
|
+
field,
|
|
202
|
+
value,
|
|
203
|
+
onChange,
|
|
204
|
+
depth
|
|
205
|
+
}) {
|
|
206
|
+
const [open, setOpen] = useState(true);
|
|
207
|
+
return /* @__PURE__ */ React.createElement("div", { className: "bdf-collapsible" }, /* @__PURE__ */ React.createElement(
|
|
208
|
+
"button",
|
|
209
|
+
{
|
|
210
|
+
type: "button",
|
|
211
|
+
className: "bdf-collapsible__header",
|
|
212
|
+
onClick: () => setOpen((o) => !o)
|
|
213
|
+
},
|
|
214
|
+
/* @__PURE__ */ React.createElement("span", { className: `bdf-collapsible__caret${open ? " bdf-collapsible__caret--open" : ""}` }, "\u25B8"),
|
|
215
|
+
field.label
|
|
216
|
+
), open && /* @__PURE__ */ React.createElement("div", { className: "bdf-collapsible__body" }, /* @__PURE__ */ React.createElement(SchemaForm, { schema: field.fields, value, onChange, depth })));
|
|
217
|
+
}
|
|
218
|
+
function TabsSection({
|
|
219
|
+
field,
|
|
220
|
+
value,
|
|
221
|
+
onChange,
|
|
222
|
+
depth
|
|
223
|
+
}) {
|
|
224
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
225
|
+
const set = useCallback(
|
|
226
|
+
(key, val) => onChange({ ...value, [key]: val }),
|
|
227
|
+
[value, onChange]
|
|
228
|
+
);
|
|
229
|
+
const tabs = field.tabs ?? [];
|
|
230
|
+
const tab = tabs[activeTab];
|
|
231
|
+
return /* @__PURE__ */ React.createElement("div", { className: "bdf-tabs" }, /* @__PURE__ */ React.createElement("div", { className: "bdf-tabs__list" }, tabs.map((t, i) => /* @__PURE__ */ React.createElement(
|
|
232
|
+
"button",
|
|
233
|
+
{
|
|
234
|
+
key: t.name ?? t.label ?? i,
|
|
235
|
+
type: "button",
|
|
236
|
+
className: `bdf-tabs__tab${i === activeTab ? " bdf-tabs__tab--active" : ""}`,
|
|
237
|
+
onClick: () => setActiveTab(i)
|
|
238
|
+
},
|
|
239
|
+
t.label
|
|
240
|
+
))), /* @__PURE__ */ React.createElement("div", { className: "bdf-tabs__panel" }, tab && (tab.name ? /* @__PURE__ */ React.createElement(SchemaForm, { schema: tab.fields, value: value[tab.name] ?? {}, onChange: (v) => set(tab.name, v), depth }) : /* @__PURE__ */ React.createElement(SchemaForm, { schema: tab.fields, value, onChange, depth }))));
|
|
183
241
|
}
|
|
184
|
-
function FieldInput({ field, value, onChange }) {
|
|
242
|
+
function FieldInput({ field, value, onChange, depth }) {
|
|
185
243
|
const label = field.label ?? field.name;
|
|
186
244
|
switch (field.type) {
|
|
187
245
|
case "text":
|
|
@@ -334,7 +392,8 @@ function FieldInput({ field, value, onChange }) {
|
|
|
334
392
|
const next = [...rows];
|
|
335
393
|
next[i] = updated;
|
|
336
394
|
onChange(next);
|
|
337
|
-
}
|
|
395
|
+
},
|
|
396
|
+
depth: depth + 1
|
|
338
397
|
}
|
|
339
398
|
), /* @__PURE__ */ React.createElement(
|
|
340
399
|
"button",
|
|
@@ -358,7 +417,7 @@ function FieldInput({ field, value, onChange }) {
|
|
|
358
417
|
case "group": {
|
|
359
418
|
const groupVal = value ?? {};
|
|
360
419
|
const subFields = field.fields ?? [];
|
|
361
|
-
return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("div", { className: "bdf-fieldset" }, /* @__PURE__ */ React.createElement("div", { className: "bdf-fieldset__header" }, label), /* @__PURE__ */ React.createElement("div", { className: "bdf-fieldset__body" }, /* @__PURE__ */ React.createElement(SchemaForm, { schema: subFields, value: groupVal, onChange }))));
|
|
420
|
+
return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("div", { className: "bdf-fieldset" }, /* @__PURE__ */ React.createElement("div", { className: "bdf-fieldset__header" }, label), /* @__PURE__ */ React.createElement("div", { className: "bdf-fieldset__body" }, /* @__PURE__ */ React.createElement(SchemaForm, { schema: subFields, value: groupVal, onChange, depth: depth + 1 }))));
|
|
362
421
|
}
|
|
363
422
|
default:
|
|
364
423
|
return /* @__PURE__ */ React.createElement("div", { style: { fontSize: 12, color: "var(--theme-elevation-400)", fontFamily: "var(--font-body)", padding: "4px 0" } }, "Unsupported field type: ", /* @__PURE__ */ React.createElement("strong", null, field.type), " (", label, ")");
|
|
@@ -379,6 +438,10 @@ function BlockDataField({ path }) {
|
|
|
379
438
|
setError(null);
|
|
380
439
|
return;
|
|
381
440
|
}
|
|
441
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(String(versionId))) {
|
|
442
|
+
setError("Invalid version ID format");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
382
445
|
setLoading(true);
|
|
383
446
|
setError(null);
|
|
384
447
|
fetch(`/api/block-definition-versions/${versionId}?depth=0`, { credentials: "same-origin" }).then((r) => {
|
|
@@ -433,20 +496,24 @@ function BlockVersionSync({ path }) {
|
|
|
433
496
|
setVersion(null);
|
|
434
497
|
return;
|
|
435
498
|
}
|
|
499
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(String(defId))) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
436
502
|
fetch(`/api/block-definitions/${String(defId)}?depth=1`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : null).then((doc) => {
|
|
437
503
|
if (!doc) return;
|
|
438
504
|
const currentVersion = doc.currentVersion;
|
|
439
505
|
if (!currentVersion) return;
|
|
440
506
|
const versionId = typeof currentVersion === "object" ? currentVersion.id : currentVersion;
|
|
441
507
|
if (versionId) setVersion(versionId);
|
|
442
|
-
}).catch(() => {
|
|
508
|
+
}).catch((err) => {
|
|
509
|
+
console.error("[Block Builder] BlockVersionSync fetch error:", err);
|
|
443
510
|
});
|
|
444
511
|
}, [blockDefValue, setVersion]);
|
|
445
512
|
return null;
|
|
446
513
|
}
|
|
447
514
|
|
|
448
515
|
// src/components/SchemaBuilderField/index.tsx
|
|
449
|
-
import
|
|
516
|
+
import React6, { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
450
517
|
import { useField as useField3 } from "@payloadcms/ui";
|
|
451
518
|
|
|
452
519
|
// src/components/SchemaBuilderField/FieldRow.tsx
|
|
@@ -1009,128 +1076,72 @@ function FieldRow({
|
|
|
1009
1076
|
)))));
|
|
1010
1077
|
}
|
|
1011
1078
|
|
|
1012
|
-
// src/components/
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1079
|
+
// src/block-builder/components/ErrorBoundary.tsx
|
|
1080
|
+
import React5, { Component } from "react";
|
|
1081
|
+
|
|
1082
|
+
// src/block-builder/store/builder.store.ts
|
|
1083
|
+
import { create } from "zustand";
|
|
1084
|
+
import { devtools, persist } from "zustand/middleware";
|
|
1085
|
+
import { immer } from "zustand/middleware/immer";
|
|
1086
|
+
|
|
1087
|
+
// src/utils/uuid.ts
|
|
1088
|
+
function uuidv4() {
|
|
1089
|
+
const c = globalThis.crypto;
|
|
1090
|
+
if (typeof c?.randomUUID === "function") {
|
|
1091
|
+
return c.randomUUID();
|
|
1020
1092
|
}
|
|
1021
|
-
|
|
1093
|
+
if (typeof c?.getRandomValues === "function") {
|
|
1094
|
+
const bytes = c.getRandomValues(new Uint8Array(16));
|
|
1095
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
1096
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
1097
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1098
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1099
|
+
}
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
"No cryptographic random source available. `crypto.randomUUID` requires a secure context (HTTPS or localhost)."
|
|
1102
|
+
);
|
|
1022
1103
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
if (
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
setFields((prev) => [
|
|
1045
|
-
...prev,
|
|
1046
|
-
{ name: "", type: "text", label: "", required: false }
|
|
1047
|
-
]);
|
|
1048
|
-
}, []);
|
|
1049
|
-
const updateField = useCallback3((index, updated) => {
|
|
1050
|
-
setFields((prev) => prev.map((f, i) => i === index ? updated : f));
|
|
1051
|
-
}, []);
|
|
1052
|
-
const removeField = useCallback3((index) => {
|
|
1053
|
-
setFields((prev) => prev.filter((_, i) => i !== index));
|
|
1054
|
-
}, []);
|
|
1055
|
-
const moveField = useCallback3((index, dir) => {
|
|
1056
|
-
setFields((prev) => {
|
|
1057
|
-
const next = [...prev];
|
|
1058
|
-
const target = index + dir;
|
|
1059
|
-
if (target < 0 || target >= next.length) return prev;
|
|
1060
|
-
[next[index], next[target]] = [next[target], next[index]];
|
|
1061
|
-
return next;
|
|
1062
|
-
});
|
|
1063
|
-
}, []);
|
|
1064
|
-
return /* @__PURE__ */ React5.createElement("div", null, /* @__PURE__ */ React5.createElement("div", { className: "sbf-section-heading" }, /* @__PURE__ */ React5.createElement("span", { className: "sbf-section-label" }, "Fields", fields.length > 0 && /* @__PURE__ */ React5.createElement(
|
|
1065
|
-
"span",
|
|
1066
|
-
{
|
|
1067
|
-
style: {
|
|
1068
|
-
marginLeft: 8,
|
|
1069
|
-
fontSize: 11,
|
|
1070
|
-
color: "var(--theme-elevation-400)",
|
|
1071
|
-
fontWeight: 400
|
|
1104
|
+
|
|
1105
|
+
// src/block-builder/store/builder.store.ts
|
|
1106
|
+
var TAB_PATH_SEP = "::";
|
|
1107
|
+
var BUILDER_PERSIST_KEY = "@nextbridgehq/payload-block-builder";
|
|
1108
|
+
function encodeTabPath(fieldId, tabIndex) {
|
|
1109
|
+
return `${fieldId}${TAB_PATH_SEP}${tabIndex}`;
|
|
1110
|
+
}
|
|
1111
|
+
function walkPath(block, parentPath, create2) {
|
|
1112
|
+
let currentFields = block.fields;
|
|
1113
|
+
for (const segment of parentPath) {
|
|
1114
|
+
const sepIndex = segment.indexOf(TAB_PATH_SEP);
|
|
1115
|
+
if (sepIndex !== -1) {
|
|
1116
|
+
const fieldId = segment.slice(0, sepIndex);
|
|
1117
|
+
const tabIndex = Number(segment.slice(sepIndex + TAB_PATH_SEP.length));
|
|
1118
|
+
const parentField2 = currentFields.find((f) => f.id === fieldId);
|
|
1119
|
+
if (!parentField2 || parentField2.type !== "tabs" || !Array.isArray(parentField2.tabs)) return null;
|
|
1120
|
+
const tab = parentField2.tabs[tabIndex];
|
|
1121
|
+
if (!tab) return null;
|
|
1122
|
+
if (!tab.fields) {
|
|
1123
|
+
if (!create2) return null;
|
|
1124
|
+
tab.fields = [];
|
|
1072
1125
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
index: i,
|
|
1082
|
-
total: fields.length,
|
|
1083
|
-
field,
|
|
1084
|
-
onChange: (updated) => updateField(i, updated),
|
|
1085
|
-
onRemove: () => removeField(i),
|
|
1086
|
-
onMoveUp: i > 0 ? () => moveField(i, -1) : void 0,
|
|
1087
|
-
onMoveDown: i < fields.length - 1 ? () => moveField(i, 1) : void 0,
|
|
1088
|
-
readOnly
|
|
1126
|
+
currentFields = tab.fields;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
const parentField = currentFields.find((f) => f.id === segment);
|
|
1130
|
+
if (!parentField) return null;
|
|
1131
|
+
if (!parentField.fields) {
|
|
1132
|
+
if (!create2) return null;
|
|
1133
|
+
parentField.fields = [];
|
|
1089
1134
|
}
|
|
1090
|
-
|
|
1135
|
+
currentFields = parentField.fields;
|
|
1136
|
+
}
|
|
1137
|
+
return currentFields;
|
|
1091
1138
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
function EditInBuilderButton() {
|
|
1098
|
-
const { id } = useDocumentInfo();
|
|
1099
|
-
const { value: slug } = useField4({ path: "slug" });
|
|
1100
|
-
if (!id || !slug) return null;
|
|
1101
|
-
return /* @__PURE__ */ React6.createElement("div", { style: { marginTop: "1rem" } }, /* @__PURE__ */ React6.createElement(
|
|
1102
|
-
"a",
|
|
1103
|
-
{
|
|
1104
|
-
href: `/block-builder?load=${slug}`,
|
|
1105
|
-
target: "_blank",
|
|
1106
|
-
rel: "noopener noreferrer",
|
|
1107
|
-
style: {
|
|
1108
|
-
display: "inline-flex",
|
|
1109
|
-
alignItems: "center",
|
|
1110
|
-
gap: "6px",
|
|
1111
|
-
padding: "8px 16px",
|
|
1112
|
-
borderRadius: "6px",
|
|
1113
|
-
border: "1px solid #16a34a",
|
|
1114
|
-
color: "#16a34a",
|
|
1115
|
-
textDecoration: "none",
|
|
1116
|
-
fontSize: "13px",
|
|
1117
|
-
fontWeight: 500,
|
|
1118
|
-
background: "transparent",
|
|
1119
|
-
cursor: "pointer"
|
|
1120
|
-
}
|
|
1121
|
-
},
|
|
1122
|
-
"Edit in Block Builder"
|
|
1123
|
-
));
|
|
1139
|
+
function getTargetFields(block, parentPath) {
|
|
1140
|
+
return walkPath(block, parentPath, false);
|
|
1141
|
+
}
|
|
1142
|
+
function ensureTargetFields(block, parentPath) {
|
|
1143
|
+
return walkPath(block, parentPath, true);
|
|
1124
1144
|
}
|
|
1125
|
-
|
|
1126
|
-
// src/block-builder/components/canvas/BuilderShell.tsx
|
|
1127
|
-
import React16, { useCallback as useCallback5, useEffect as useEffect6, useState as useState8 } from "react";
|
|
1128
|
-
|
|
1129
|
-
// src/block-builder/store/builder.store.ts
|
|
1130
|
-
import { create } from "zustand";
|
|
1131
|
-
import { devtools, persist } from "zustand/middleware";
|
|
1132
|
-
import { immer } from "zustand/middleware/immer";
|
|
1133
|
-
import { v4 as uuidv4 } from "uuid";
|
|
1134
1145
|
function createDefaultField(type) {
|
|
1135
1146
|
const base = {
|
|
1136
1147
|
id: uuidv4(),
|
|
@@ -1141,7 +1152,6 @@ function createDefaultField(type) {
|
|
|
1141
1152
|
};
|
|
1142
1153
|
switch (type) {
|
|
1143
1154
|
case "select":
|
|
1144
|
-
case "radio":
|
|
1145
1155
|
return {
|
|
1146
1156
|
...base,
|
|
1147
1157
|
options: [
|
|
@@ -1150,15 +1160,31 @@ function createDefaultField(type) {
|
|
|
1150
1160
|
]
|
|
1151
1161
|
};
|
|
1152
1162
|
case "relationship":
|
|
1153
|
-
return { ...base,
|
|
1163
|
+
return { ...base, collection: "", hasMany: false };
|
|
1154
1164
|
case "array":
|
|
1155
1165
|
return { ...base, fields: [] };
|
|
1156
1166
|
case "group":
|
|
1157
1167
|
return { ...base, fields: [] };
|
|
1168
|
+
case "row":
|
|
1169
|
+
return { ...base, fields: [] };
|
|
1170
|
+
case "collapsible":
|
|
1171
|
+
return { ...base, label: base.label ?? "Collapsible Section", fields: [] };
|
|
1172
|
+
case "tabs":
|
|
1173
|
+
return { ...base, tabs: [{ id: uuidv4(), label: "Tab 1", fields: [] }] };
|
|
1158
1174
|
default:
|
|
1159
1175
|
return base;
|
|
1160
1176
|
}
|
|
1161
1177
|
}
|
|
1178
|
+
function regenerateFieldIds(fields) {
|
|
1179
|
+
return fields.map((f) => {
|
|
1180
|
+
const next = { ...f, id: uuidv4() };
|
|
1181
|
+
if (next.fields) next.fields = regenerateFieldIds(next.fields);
|
|
1182
|
+
if (next.tabs) {
|
|
1183
|
+
next.tabs = next.tabs.map((tab) => ({ ...tab, fields: regenerateFieldIds(tab.fields) }));
|
|
1184
|
+
}
|
|
1185
|
+
return next;
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1162
1188
|
function createDefaultBlock() {
|
|
1163
1189
|
return {
|
|
1164
1190
|
id: uuidv4(),
|
|
@@ -1182,11 +1208,13 @@ var useBuilderStore = create()(
|
|
|
1182
1208
|
isReadOnly: false,
|
|
1183
1209
|
loadedVersionId: null,
|
|
1184
1210
|
blockSlug: null,
|
|
1211
|
+
activeParentPath: [],
|
|
1185
1212
|
addBlock: () => set((state) => {
|
|
1186
1213
|
const block = createDefaultBlock();
|
|
1187
1214
|
state.blocks.push(block);
|
|
1188
1215
|
state.activeBlockId = block.id;
|
|
1189
1216
|
state.activeFieldId = null;
|
|
1217
|
+
state.activeParentPath = [];
|
|
1190
1218
|
state.isDirty = true;
|
|
1191
1219
|
}),
|
|
1192
1220
|
removeBlock: (blockId) => set((state) => {
|
|
@@ -1194,6 +1222,7 @@ var useBuilderStore = create()(
|
|
|
1194
1222
|
if (state.activeBlockId === blockId) {
|
|
1195
1223
|
state.activeBlockId = state.blocks[0]?.id ?? null;
|
|
1196
1224
|
state.activeFieldId = null;
|
|
1225
|
+
state.activeParentPath = [];
|
|
1197
1226
|
}
|
|
1198
1227
|
state.isDirty = true;
|
|
1199
1228
|
}),
|
|
@@ -1205,15 +1234,16 @@ var useBuilderStore = create()(
|
|
|
1205
1234
|
setActiveBlock: (blockId) => set((state) => {
|
|
1206
1235
|
state.activeBlockId = blockId;
|
|
1207
1236
|
state.activeFieldId = null;
|
|
1237
|
+
state.activeParentPath = [];
|
|
1208
1238
|
}),
|
|
1209
1239
|
duplicateBlock: (blockId) => set((state) => {
|
|
1210
1240
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1211
1241
|
if (!block) return;
|
|
1212
1242
|
const clone = JSON.parse(JSON.stringify(block));
|
|
1213
1243
|
clone.id = uuidv4();
|
|
1214
|
-
clone.slug = `${block.slug}
|
|
1244
|
+
clone.slug = `${block.slug}-copy`;
|
|
1215
1245
|
clone.interfaceName = block.interfaceName ? `${block.interfaceName}Copy` : void 0;
|
|
1216
|
-
clone.fields = clone.fields
|
|
1246
|
+
clone.fields = regenerateFieldIds(clone.fields);
|
|
1217
1247
|
const idx = state.blocks.findIndex((b) => b.id === blockId);
|
|
1218
1248
|
state.blocks.splice(idx + 1, 0, clone);
|
|
1219
1249
|
state.activeBlockId = clone.id;
|
|
@@ -1222,36 +1252,47 @@ var useBuilderStore = create()(
|
|
|
1222
1252
|
addField: (blockId, type) => set((state) => {
|
|
1223
1253
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1224
1254
|
if (!block) return;
|
|
1255
|
+
const targetFields = ensureTargetFields(block, state.activeParentPath);
|
|
1256
|
+
if (!targetFields) return;
|
|
1225
1257
|
const field = createDefaultField(type);
|
|
1226
|
-
|
|
1258
|
+
targetFields.push(field);
|
|
1227
1259
|
state.activeFieldId = field.id;
|
|
1228
1260
|
state.isDirty = true;
|
|
1229
1261
|
}),
|
|
1230
1262
|
removeField: (blockId, fieldId) => set((state) => {
|
|
1231
1263
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1232
1264
|
if (!block) return;
|
|
1233
|
-
|
|
1265
|
+
const targetFields = ensureTargetFields(block, state.activeParentPath);
|
|
1266
|
+
if (!targetFields) return;
|
|
1267
|
+
const index = targetFields.findIndex((f) => f.id === fieldId);
|
|
1268
|
+
if (index !== -1) {
|
|
1269
|
+
targetFields.splice(index, 1);
|
|
1270
|
+
}
|
|
1234
1271
|
if (state.activeFieldId === fieldId) state.activeFieldId = null;
|
|
1235
1272
|
state.isDirty = true;
|
|
1236
1273
|
}),
|
|
1237
1274
|
updateField: (blockId, fieldId, updates) => set((state) => {
|
|
1238
1275
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1239
1276
|
if (!block) return;
|
|
1240
|
-
const
|
|
1277
|
+
const targetFields = ensureTargetFields(block, state.activeParentPath);
|
|
1278
|
+
if (!targetFields) return;
|
|
1279
|
+
const field = targetFields.find((f) => f.id === fieldId);
|
|
1241
1280
|
if (field) Object.assign(field, updates);
|
|
1242
1281
|
state.isDirty = true;
|
|
1243
1282
|
}),
|
|
1244
1283
|
reorderFields: (blockId, fromIndex, toIndex) => set((state) => {
|
|
1245
1284
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1246
1285
|
if (!block) return;
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1286
|
+
const targetFields = ensureTargetFields(block, state.activeParentPath);
|
|
1287
|
+
if (!targetFields) return;
|
|
1288
|
+
const [moved] = targetFields.splice(fromIndex, 1);
|
|
1289
|
+
targetFields.splice(toIndex, 0, moved);
|
|
1249
1290
|
state.isDirty = true;
|
|
1250
1291
|
}),
|
|
1251
1292
|
setActiveField: (fieldId) => set((state) => {
|
|
1252
1293
|
state.activeFieldId = fieldId;
|
|
1253
1294
|
}),
|
|
1254
|
-
reset: () => set(() => ({ ...initialState, isReadOnly: false, loadedVersionId: null, blockSlug: null })),
|
|
1295
|
+
reset: () => set(() => ({ ...initialState, isReadOnly: false, loadedVersionId: null, blockSlug: null, activeParentPath: [] })),
|
|
1255
1296
|
markClean: () => set((state) => {
|
|
1256
1297
|
state.isDirty = false;
|
|
1257
1298
|
}),
|
|
@@ -1259,6 +1300,7 @@ var useBuilderStore = create()(
|
|
|
1259
1300
|
state.blocks = [block];
|
|
1260
1301
|
state.activeBlockId = block.id;
|
|
1261
1302
|
state.activeFieldId = null;
|
|
1303
|
+
state.activeParentPath = [];
|
|
1262
1304
|
state.isDirty = false;
|
|
1263
1305
|
}),
|
|
1264
1306
|
setVersionMeta: (versionId, isReadOnly) => set((state) => {
|
|
@@ -1267,10 +1309,36 @@ var useBuilderStore = create()(
|
|
|
1267
1309
|
}),
|
|
1268
1310
|
setBlockSlug: (slug) => set((state) => {
|
|
1269
1311
|
state.blockSlug = slug;
|
|
1312
|
+
}),
|
|
1313
|
+
pushParentPath: (fieldId) => set((state) => {
|
|
1314
|
+
state.activeParentPath.push(fieldId);
|
|
1315
|
+
state.activeFieldId = null;
|
|
1316
|
+
}),
|
|
1317
|
+
popParentPath: () => set((state) => {
|
|
1318
|
+
state.activeParentPath.pop();
|
|
1319
|
+
state.activeFieldId = null;
|
|
1320
|
+
}),
|
|
1321
|
+
// Jump directly to an ancestor level -- `depth` is the number of
|
|
1322
|
+
// segments to keep, so breadcrumb index `i` maps to `i + 1`.
|
|
1323
|
+
truncateParentPath: (depth) => set((state) => {
|
|
1324
|
+
if (depth < 0 || depth >= state.activeParentPath.length) return;
|
|
1325
|
+
state.activeParentPath = state.activeParentPath.slice(0, depth);
|
|
1326
|
+
state.activeFieldId = null;
|
|
1327
|
+
}),
|
|
1328
|
+
resetParentPath: () => set((state) => {
|
|
1329
|
+
state.activeParentPath = [];
|
|
1330
|
+
state.activeFieldId = null;
|
|
1270
1331
|
})
|
|
1271
1332
|
})),
|
|
1272
1333
|
{
|
|
1273
|
-
name:
|
|
1334
|
+
name: BUILDER_PERSIST_KEY,
|
|
1335
|
+
version: 1,
|
|
1336
|
+
migrate: (persistedState, version) => {
|
|
1337
|
+
if (version === 0) {
|
|
1338
|
+
return { blocks: [], activeBlockId: null };
|
|
1339
|
+
}
|
|
1340
|
+
return persistedState;
|
|
1341
|
+
},
|
|
1274
1342
|
partialize: (state) => ({
|
|
1275
1343
|
blocks: state.blocks,
|
|
1276
1344
|
activeBlockId: state.activeBlockId
|
|
@@ -1280,163 +1348,191 @@ var useBuilderStore = create()(
|
|
|
1280
1348
|
)
|
|
1281
1349
|
);
|
|
1282
1350
|
|
|
1283
|
-
// src/block-builder/components/
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
}).map(({ id: _id, relationTo, ...f }) => ({
|
|
1305
|
-
...f,
|
|
1306
|
-
type: TYPE_MAP[f.type] ?? f.type,
|
|
1307
|
-
// normalizer reads `collection`, block-builder stores `relationTo`
|
|
1308
|
-
...relationTo ? { collection: relationTo } : {}
|
|
1309
|
-
}));
|
|
1310
|
-
return {
|
|
1311
|
-
blockSlug: normalizeSlug(block.slug),
|
|
1312
|
-
name: block.labels?.singular ?? block.slug,
|
|
1313
|
-
schema: { fields },
|
|
1314
|
-
changelog: "Created via block builder"
|
|
1315
|
-
};
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// src/block-builder/lib/codegen.ts
|
|
1319
|
-
function indent(n) {
|
|
1320
|
-
return " ".repeat(n);
|
|
1321
|
-
}
|
|
1322
|
-
function escStr(s) {
|
|
1323
|
-
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
1324
|
-
}
|
|
1325
|
-
function fieldToCode(field, depth = 1) {
|
|
1326
|
-
const pad = indent(depth);
|
|
1327
|
-
const innerPad = indent(depth + 1);
|
|
1328
|
-
const lines = [];
|
|
1329
|
-
lines.push(`${pad}name: '${escStr(field.name)}'`);
|
|
1330
|
-
lines.push(`${pad}type: '${field.type}'`);
|
|
1331
|
-
if (field.label) lines.push(`${pad}label: '${escStr(field.label)}'`);
|
|
1332
|
-
if (field.required) lines.push(`${pad}required: true`);
|
|
1333
|
-
if (field.unique) lines.push(`${pad}unique: true`);
|
|
1334
|
-
if (field.localized) lines.push(`${pad}localized: true`);
|
|
1335
|
-
if (field.defaultValue !== void 0) {
|
|
1336
|
-
const val = typeof field.defaultValue === "string" ? `'${escStr(String(field.defaultValue))}'` : field.defaultValue;
|
|
1337
|
-
lines.push(`${pad}defaultValue: ${val}`);
|
|
1338
|
-
}
|
|
1339
|
-
if (field.type === "richText") {
|
|
1340
|
-
lines.push(`${pad}editor: lexicalEditor({})`);
|
|
1341
|
-
}
|
|
1342
|
-
if (field.options && field.options.length > 0) {
|
|
1343
|
-
const opts = field.options.map((o) => `{ label: '${escStr(o.label)}', value: '${escStr(o.value)}' }`).join(`, `);
|
|
1344
|
-
lines.push(`${pad}options: [${opts}]`);
|
|
1351
|
+
// src/block-builder/components/ErrorBoundary.tsx
|
|
1352
|
+
var ErrorBoundary = class extends Component {
|
|
1353
|
+
constructor() {
|
|
1354
|
+
super(...arguments);
|
|
1355
|
+
this.state = {
|
|
1356
|
+
hasError: false,
|
|
1357
|
+
error: null
|
|
1358
|
+
};
|
|
1359
|
+
/**
|
|
1360
|
+
* Clears persisted builder state before reloading. "Try again" alone only
|
|
1361
|
+
* resets the boundary, so if the cause is corrupt persisted state the next
|
|
1362
|
+
* render throws immediately -- this gives the user a way out of that loop.
|
|
1363
|
+
*/
|
|
1364
|
+
this.handleResetState = () => {
|
|
1365
|
+
try {
|
|
1366
|
+
window.localStorage.removeItem(BUILDER_PERSIST_KEY);
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
console.error("[Block Builder] Could not clear persisted state:", err);
|
|
1369
|
+
}
|
|
1370
|
+
window.location.reload();
|
|
1371
|
+
};
|
|
1345
1372
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1373
|
+
static getDerivedStateFromError(error) {
|
|
1374
|
+
return { hasError: true, error };
|
|
1348
1375
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1376
|
+
componentDidCatch(error, errorInfo) {
|
|
1377
|
+
console.error("[Block Builder] Uncaught error:", error, errorInfo);
|
|
1351
1378
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1379
|
+
render() {
|
|
1380
|
+
if (this.state.hasError) {
|
|
1381
|
+
return /* @__PURE__ */ React5.createElement("div", { className: "bb-error-boundary", role: "alert" }, /* @__PURE__ */ React5.createElement("h2", { className: "bb-error-boundary__title" }, "Something went wrong in the Block Builder."), /* @__PURE__ */ React5.createElement("details", { className: "bb-error-boundary__details" }, /* @__PURE__ */ React5.createElement("summary", null, "Error details"), /* @__PURE__ */ React5.createElement("pre", { className: "bb-error-boundary__trace" }, this.state.error?.toString())), /* @__PURE__ */ React5.createElement("div", { className: "bb-error-boundary__actions" }, /* @__PURE__ */ React5.createElement(
|
|
1382
|
+
"button",
|
|
1383
|
+
{
|
|
1384
|
+
type: "button",
|
|
1385
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
1386
|
+
className: "bb-error-boundary__btn bb-error-boundary__btn--primary"
|
|
1387
|
+
},
|
|
1388
|
+
"Try again"
|
|
1389
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1390
|
+
"button",
|
|
1391
|
+
{
|
|
1392
|
+
type: "button",
|
|
1393
|
+
onClick: this.handleResetState,
|
|
1394
|
+
className: "bb-error-boundary__btn"
|
|
1395
|
+
},
|
|
1396
|
+
"Reset builder state"
|
|
1397
|
+
)), /* @__PURE__ */ React5.createElement("p", { className: "bb-error-boundary__hint" }, "\u201CReset builder state\u201D discards unsaved local edits and reloads. Published versions are stored in the database and are not affected."));
|
|
1398
|
+
}
|
|
1399
|
+
return this.props.children;
|
|
1361
1400
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
// src/components/SchemaBuilderField/index.tsx
|
|
1404
|
+
function parseSchema(raw) {
|
|
1405
|
+
try {
|
|
1406
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
1407
|
+
if (!parsed) return [];
|
|
1408
|
+
if (Array.isArray(parsed)) return parsed;
|
|
1409
|
+
if (parsed?.fields && Array.isArray(parsed.fields)) return parsed.fields;
|
|
1410
|
+
} catch {
|
|
1371
1411
|
}
|
|
1372
|
-
return
|
|
1412
|
+
return [];
|
|
1373
1413
|
}
|
|
1374
|
-
function
|
|
1375
|
-
const
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
)
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1414
|
+
function SchemaBuilderField({ path, readOnly }) {
|
|
1415
|
+
const { value, setValue } = useField3({ path });
|
|
1416
|
+
const setValueRef = useRef3(setValue);
|
|
1417
|
+
useEffect3(() => {
|
|
1418
|
+
setValueRef.current = setValue;
|
|
1419
|
+
});
|
|
1420
|
+
const [fields, setFields] = useState3(() => parseSchema(value));
|
|
1421
|
+
const hasExistingValue = value !== void 0 && value !== null;
|
|
1422
|
+
const [hydrated, setHydrated] = useState3(!hasExistingValue);
|
|
1423
|
+
useEffect3(() => {
|
|
1424
|
+
if (hydrated) return;
|
|
1425
|
+
if (value !== void 0 && value !== null) {
|
|
1426
|
+
setFields(parseSchema(value));
|
|
1427
|
+
setHydrated(true);
|
|
1428
|
+
}
|
|
1429
|
+
}, [value, hydrated]);
|
|
1430
|
+
useEffect3(() => {
|
|
1431
|
+
if (!hydrated) return;
|
|
1432
|
+
setValueRef.current({ fields });
|
|
1433
|
+
}, [fields, hydrated]);
|
|
1434
|
+
const addField = useCallback3(() => {
|
|
1435
|
+
setFields((prev) => [
|
|
1436
|
+
...prev,
|
|
1437
|
+
{ name: "", type: "text", label: "", required: false }
|
|
1438
|
+
]);
|
|
1439
|
+
}, []);
|
|
1440
|
+
const updateField = useCallback3((index, updated) => {
|
|
1441
|
+
setFields((prev) => prev.map((f, i) => i === index ? updated : f));
|
|
1442
|
+
}, []);
|
|
1443
|
+
const removeField = useCallback3((index) => {
|
|
1444
|
+
setFields((prev) => prev.filter((_, i) => i !== index));
|
|
1445
|
+
}, []);
|
|
1446
|
+
const moveField = useCallback3((index, dir) => {
|
|
1447
|
+
setFields((prev) => {
|
|
1448
|
+
const next = [...prev];
|
|
1449
|
+
const target = index + dir;
|
|
1450
|
+
if (target < 0 || target >= next.length) return prev;
|
|
1451
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
1452
|
+
return next;
|
|
1453
|
+
});
|
|
1454
|
+
}, []);
|
|
1455
|
+
return /* @__PURE__ */ React6.createElement(ErrorBoundary, null, /* @__PURE__ */ React6.createElement("div", null, /* @__PURE__ */ React6.createElement("div", { className: "sbf-section-heading" }, /* @__PURE__ */ React6.createElement("span", { className: "sbf-section-label" }, "Fields", fields.length > 0 && /* @__PURE__ */ React6.createElement(
|
|
1456
|
+
"span",
|
|
1457
|
+
{
|
|
1458
|
+
style: {
|
|
1459
|
+
marginLeft: 8,
|
|
1460
|
+
fontSize: 11,
|
|
1461
|
+
color: "var(--theme-elevation-400)",
|
|
1462
|
+
fontWeight: 400
|
|
1463
|
+
}
|
|
1464
|
+
},
|
|
1465
|
+
"(",
|
|
1466
|
+
fields.length,
|
|
1467
|
+
")"
|
|
1468
|
+
))), fields.length === 0 ? /* @__PURE__ */ React6.createElement("div", { className: "sbf-empty" }, "No fields defined. Click \u201CAdd Field\u201D below to add the first field to this block schema.") : /* @__PURE__ */ React6.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "calc(var(--base, 16px) / 2)" } }, fields.map((field, i) => /* @__PURE__ */ React6.createElement(
|
|
1469
|
+
FieldRow,
|
|
1470
|
+
{
|
|
1471
|
+
key: i,
|
|
1472
|
+
index: i,
|
|
1473
|
+
total: fields.length,
|
|
1474
|
+
field,
|
|
1475
|
+
onChange: (updated) => updateField(i, updated),
|
|
1476
|
+
onRemove: () => removeField(i),
|
|
1477
|
+
onMoveUp: i > 0 ? () => moveField(i, -1) : void 0,
|
|
1478
|
+
onMoveDown: i < fields.length - 1 ? () => moveField(i, 1) : void 0,
|
|
1479
|
+
readOnly
|
|
1480
|
+
}
|
|
1481
|
+
))), !readOnly && /* @__PURE__ */ React6.createElement("div", { style: { marginTop: "calc(var(--base, 16px) / 2)", marginBottom: "calc(var(--base, 16px) / 2)" } }, /* @__PURE__ */ React6.createElement("button", { type: "button", className: "sbf-add-btn", onClick: addField }, /* @__PURE__ */ React6.createElement("span", { className: "sbf-add-btn__icon" }, "+"), "Add Field"))));
|
|
1422
1482
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
""
|
|
1434
|
-
|
|
1435
|
-
|
|
1483
|
+
|
|
1484
|
+
// src/components/EditInBuilderButton/index.tsx
|
|
1485
|
+
import React7 from "react";
|
|
1486
|
+
import { useDocumentInfo } from "@payloadcms/ui";
|
|
1487
|
+
import { useField as useField4 } from "@payloadcms/ui";
|
|
1488
|
+
function EditInBuilderButton() {
|
|
1489
|
+
const { id } = useDocumentInfo();
|
|
1490
|
+
const { value: slug } = useField4({ path: "slug" });
|
|
1491
|
+
if (!id || !slug) return null;
|
|
1492
|
+
return /* @__PURE__ */ React7.createElement("div", { style: { marginTop: "1rem" } }, /* @__PURE__ */ React7.createElement(
|
|
1493
|
+
"a",
|
|
1494
|
+
{
|
|
1495
|
+
href: `/block-builder?load=${slug}`,
|
|
1496
|
+
target: "_blank",
|
|
1497
|
+
rel: "noopener noreferrer",
|
|
1498
|
+
style: {
|
|
1499
|
+
display: "inline-flex",
|
|
1500
|
+
alignItems: "center",
|
|
1501
|
+
gap: "6px",
|
|
1502
|
+
padding: "8px 16px",
|
|
1503
|
+
borderRadius: "6px",
|
|
1504
|
+
border: "1px solid #16a34a",
|
|
1505
|
+
color: "#16a34a",
|
|
1506
|
+
textDecoration: "none",
|
|
1507
|
+
fontSize: "13px",
|
|
1508
|
+
fontWeight: 500,
|
|
1509
|
+
background: "transparent",
|
|
1510
|
+
cursor: "pointer"
|
|
1511
|
+
}
|
|
1512
|
+
},
|
|
1513
|
+
"Edit in Block Builder"
|
|
1514
|
+
));
|
|
1436
1515
|
}
|
|
1437
1516
|
|
|
1517
|
+
// src/block-builder/components/canvas/BuilderShell.tsx
|
|
1518
|
+
import React18, { useCallback as useCallback5, useEffect as useEffect6, useState as useState9 } from "react";
|
|
1519
|
+
|
|
1438
1520
|
// src/block-builder/components/canvas/TopBar.tsx
|
|
1439
|
-
|
|
1521
|
+
import React8, { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
|
|
1522
|
+
import { Blocks, ChevronDown, X } from "lucide-react";
|
|
1523
|
+
function ensureFieldIds(fields) {
|
|
1524
|
+
if (!Array.isArray(fields)) return [];
|
|
1525
|
+
return fields.map((field) => {
|
|
1526
|
+
const f = { ...field };
|
|
1527
|
+
if (!f.id) f.id = uuidv4();
|
|
1528
|
+
if (Array.isArray(f.fields)) f.fields = ensureFieldIds(f.fields);
|
|
1529
|
+
if (Array.isArray(f.tabs)) {
|
|
1530
|
+
f.tabs = f.tabs.map((tab) => ({ ...tab, fields: ensureFieldIds(tab.fields) }));
|
|
1531
|
+
}
|
|
1532
|
+
return f;
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersionId, onVersionSelect, onRestoreVersion, onAfterPublish, notification, onSetNotification, previewOpen, onTogglePreview }) {
|
|
1440
1536
|
const blocks = useBuilderStore((s) => s.blocks);
|
|
1441
1537
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1442
1538
|
const activeBlock = blocks.find((b) => b.id === activeBlockId);
|
|
@@ -1444,11 +1540,13 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1444
1540
|
const isDirty = useBuilderStore((s) => s.isDirty);
|
|
1445
1541
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
1446
1542
|
const setVersionMeta = useBuilderStore((s) => s.setVersionMeta);
|
|
1543
|
+
const loadBlock = useBuilderStore((s) => s.loadBlock);
|
|
1447
1544
|
const setNotification = onSetNotification;
|
|
1448
1545
|
const [versionDropdownOpen, setVersionDropdownOpen] = useState4(false);
|
|
1449
1546
|
const [blockPickerOpen, setBlockPickerOpen] = useState4(false);
|
|
1450
1547
|
const dropdownRef = useRef4(null);
|
|
1451
1548
|
const blockPickerRef = useRef4(null);
|
|
1549
|
+
const fileInputRef = useRef4(null);
|
|
1452
1550
|
const selectedVersion = versions.find((v) => v.id === selectedVersionId);
|
|
1453
1551
|
const currentVersion = versions.find((v) => v.isCurrent);
|
|
1454
1552
|
const activeBlockDef = blockDefs.find((b) => b.slug === activeSlug);
|
|
@@ -1474,10 +1572,17 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1474
1572
|
if (!activeBlock || isReadOnly) return false;
|
|
1475
1573
|
setNotification({ status: "publishing" });
|
|
1476
1574
|
try {
|
|
1477
|
-
const
|
|
1575
|
+
const normalizeSlug = (s) => s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1576
|
+
const publishedSlug = normalizeSlug(activeBlock.slug);
|
|
1577
|
+
const req = {
|
|
1578
|
+
blockSlug: publishedSlug,
|
|
1579
|
+
name: activeBlock.labels?.singular ?? activeBlock.slug,
|
|
1580
|
+
schema: { fields: activeBlock.fields },
|
|
1581
|
+
changelog: "Created via block builder"
|
|
1582
|
+
};
|
|
1478
1583
|
const res = await fetch("/api/blocks/save", {
|
|
1479
1584
|
method: "POST",
|
|
1480
|
-
headers: { "Content-Type": "application/json" },
|
|
1585
|
+
headers: { "Content-Type": "application/json", "X-Block-Builder": "1" },
|
|
1481
1586
|
body: JSON.stringify(req)
|
|
1482
1587
|
});
|
|
1483
1588
|
const json = await res.json();
|
|
@@ -1488,7 +1593,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1488
1593
|
status: "success",
|
|
1489
1594
|
msg: `v${json.versionNumber ?? "?"} published successfully!`
|
|
1490
1595
|
});
|
|
1491
|
-
onAfterPublish();
|
|
1596
|
+
onAfterPublish(publishedSlug);
|
|
1492
1597
|
return true;
|
|
1493
1598
|
} else {
|
|
1494
1599
|
setNotification({
|
|
@@ -1507,35 +1612,54 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1507
1612
|
return false;
|
|
1508
1613
|
}
|
|
1509
1614
|
}
|
|
1510
|
-
function
|
|
1511
|
-
if (
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1615
|
+
function handleExportJson() {
|
|
1616
|
+
if (!activeBlock) return;
|
|
1617
|
+
const blob = new Blob([JSON.stringify(activeBlock, null, 2)], { type: "application/json" });
|
|
1618
|
+
const url = URL.createObjectURL(blob);
|
|
1619
|
+
const a = document.createElement("a");
|
|
1620
|
+
a.href = url;
|
|
1621
|
+
a.download = `${activeBlock.slug}-schema.json`;
|
|
1622
|
+
document.body.appendChild(a);
|
|
1623
|
+
a.click();
|
|
1624
|
+
document.body.removeChild(a);
|
|
1625
|
+
URL.revokeObjectURL(url);
|
|
1626
|
+
}
|
|
1627
|
+
function handleImportJson(e) {
|
|
1628
|
+
const file = e.target.files?.[0];
|
|
1629
|
+
if (!file) return;
|
|
1630
|
+
const reader = new FileReader();
|
|
1631
|
+
reader.onload = (event) => {
|
|
1632
|
+
try {
|
|
1633
|
+
const json = JSON.parse(event.target?.result);
|
|
1634
|
+
if (json && json.slug && Array.isArray(json.fields)) {
|
|
1635
|
+
if (!json.id) json.id = uuidv4();
|
|
1636
|
+
json.fields = ensureFieldIds(json.fields);
|
|
1637
|
+
loadBlock(json);
|
|
1638
|
+
setNotification({ status: "success", msg: "Block imported successfully!" });
|
|
1639
|
+
} else {
|
|
1640
|
+
setNotification({ status: "error", title: "Invalid JSON", errors: ["The file does not contain a valid block schema."] });
|
|
1641
|
+
}
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
setNotification({ status: "error", title: "Parse Error", errors: ["Could not parse JSON file."] });
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
reader.readAsText(file);
|
|
1647
|
+
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
1524
1648
|
}
|
|
1525
1649
|
function formatDate(iso) {
|
|
1526
1650
|
return new Date(iso).toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
1527
1651
|
}
|
|
1528
|
-
return /* @__PURE__ */
|
|
1652
|
+
return /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement("div", { className: "bb-topbar" }, /* @__PURE__ */ React8.createElement("div", { className: "bb-topbar__brand" }, /* @__PURE__ */ React8.createElement("span", { className: "bb-topbar__title" }, "Block Builder"), isDirty && !isReadOnly && /* @__PURE__ */ React8.createElement("span", { className: "bb-topbar__dirty" }, "* unsaved")), /* @__PURE__ */ React8.createElement("div", { className: "bb-topbar__selectors" }, blockDefs.length > 0 && /* @__PURE__ */ React8.createElement("div", { className: "bb-block-picker", ref: blockPickerRef }, /* @__PURE__ */ React8.createElement(
|
|
1529
1653
|
"button",
|
|
1530
1654
|
{
|
|
1531
1655
|
type: "button",
|
|
1532
1656
|
className: "bb-block-picker__trigger",
|
|
1533
1657
|
onClick: () => setBlockPickerOpen((o) => !o)
|
|
1534
1658
|
},
|
|
1535
|
-
/* @__PURE__ */
|
|
1536
|
-
/* @__PURE__ */
|
|
1537
|
-
/* @__PURE__ */
|
|
1538
|
-
), blockPickerOpen && /* @__PURE__ */
|
|
1659
|
+
/* @__PURE__ */ React8.createElement(Blocks, { size: 14, strokeWidth: 1.75, className: "bb-block-picker__icon" }),
|
|
1660
|
+
/* @__PURE__ */ React8.createElement("span", null, activeBlockDef?.name ?? activeSlug ?? "Select a block"),
|
|
1661
|
+
/* @__PURE__ */ React8.createElement(ChevronDown, { size: 14, strokeWidth: 1.75, className: "bb-version-selector__chevron" })
|
|
1662
|
+
), blockPickerOpen && /* @__PURE__ */ React8.createElement("div", { className: "bb-block-picker__dropdown" }, /* @__PURE__ */ React8.createElement("div", { className: "bb-version-dropdown__header" }, "Block Definitions"), blockDefs.map((b) => /* @__PURE__ */ React8.createElement(
|
|
1539
1663
|
"button",
|
|
1540
1664
|
{
|
|
1541
1665
|
key: b.id,
|
|
@@ -1546,20 +1670,20 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1546
1670
|
onBlockSelect(b.slug);
|
|
1547
1671
|
}
|
|
1548
1672
|
},
|
|
1549
|
-
/* @__PURE__ */
|
|
1550
|
-
/* @__PURE__ */
|
|
1551
|
-
)))), versions.length > 0 && /* @__PURE__ */
|
|
1673
|
+
/* @__PURE__ */ React8.createElement("span", { className: "bb-block-picker__item-name" }, b.name),
|
|
1674
|
+
/* @__PURE__ */ React8.createElement("span", { className: "bb-version-dropdown__meta" }, b.slug)
|
|
1675
|
+
)))), versions.length > 0 && /* @__PURE__ */ React8.createElement("div", { className: "bb-version-selector", ref: dropdownRef }, /* @__PURE__ */ React8.createElement(
|
|
1552
1676
|
"button",
|
|
1553
1677
|
{
|
|
1554
1678
|
type: "button",
|
|
1555
1679
|
className: `bb-version-selector__trigger${isReadOnly ? " bb-version-selector__trigger--readonly" : ""}`,
|
|
1556
1680
|
onClick: () => setVersionDropdownOpen((o) => !o)
|
|
1557
1681
|
},
|
|
1558
|
-
/* @__PURE__ */
|
|
1559
|
-
/* @__PURE__ */
|
|
1560
|
-
selectedVersion?.isCurrent && /* @__PURE__ */
|
|
1561
|
-
/* @__PURE__ */
|
|
1562
|
-
), versionDropdownOpen && /* @__PURE__ */
|
|
1682
|
+
/* @__PURE__ */ React8.createElement("span", { className: `bb-version-selector__dot${selectedVersion?.isCurrent ? " bb-version-selector__dot--current" : " bb-version-selector__dot--old"}` }),
|
|
1683
|
+
/* @__PURE__ */ React8.createElement("span", null, selectedVersion?.label ?? `v${selectedVersion?.versionNumber ?? "?"}`),
|
|
1684
|
+
selectedVersion?.isCurrent && /* @__PURE__ */ React8.createElement("span", { className: "bb-version-selector__badge" }, "current"),
|
|
1685
|
+
/* @__PURE__ */ React8.createElement(ChevronDown, { size: 14, strokeWidth: 1.75, className: "bb-version-selector__chevron" })
|
|
1686
|
+
), versionDropdownOpen && /* @__PURE__ */ React8.createElement("div", { className: "bb-version-dropdown" }, /* @__PURE__ */ React8.createElement("div", { className: "bb-version-dropdown__header" }, "Version History"), versions.map((v) => /* @__PURE__ */ React8.createElement(
|
|
1563
1687
|
"button",
|
|
1564
1688
|
{
|
|
1565
1689
|
key: v.id,
|
|
@@ -1570,20 +1694,45 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1570
1694
|
onVersionSelect(v.id);
|
|
1571
1695
|
}
|
|
1572
1696
|
},
|
|
1573
|
-
/* @__PURE__ */
|
|
1574
|
-
/* @__PURE__ */
|
|
1575
|
-
/* @__PURE__ */
|
|
1576
|
-
))))), /* @__PURE__ */
|
|
1697
|
+
/* @__PURE__ */ React8.createElement("span", { className: `bb-version-selector__dot${v.isCurrent ? " bb-version-selector__dot--current" : " bb-version-selector__dot--old"}` }),
|
|
1698
|
+
/* @__PURE__ */ React8.createElement("span", { className: "bb-version-dropdown__label" }, v.label, v.isCurrent && /* @__PURE__ */ React8.createElement("span", { className: "bb-version-selector__badge" }, "current")),
|
|
1699
|
+
/* @__PURE__ */ React8.createElement("span", { className: "bb-version-dropdown__meta" }, v.changelog ? `${v.changelog.slice(0, 32)}${v.changelog.length > 32 ? "..." : ""}` : formatDate(v.createdAt))
|
|
1700
|
+
))))), /* @__PURE__ */ React8.createElement("div", { className: "bb-topbar__actions" }, /* @__PURE__ */ React8.createElement(
|
|
1701
|
+
"input",
|
|
1702
|
+
{
|
|
1703
|
+
type: "file",
|
|
1704
|
+
accept: ".json",
|
|
1705
|
+
ref: fileInputRef,
|
|
1706
|
+
style: { display: "none" },
|
|
1707
|
+
onChange: handleImportJson
|
|
1708
|
+
}
|
|
1709
|
+
), /* @__PURE__ */ React8.createElement(
|
|
1577
1710
|
"button",
|
|
1578
1711
|
{
|
|
1579
1712
|
type: "button",
|
|
1580
|
-
onClick:
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1713
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1714
|
+
className: "bb-btn bb-btn--secondary"
|
|
1715
|
+
},
|
|
1716
|
+
"Import JSON"
|
|
1717
|
+
), /* @__PURE__ */ React8.createElement(
|
|
1718
|
+
"button",
|
|
1719
|
+
{
|
|
1720
|
+
type: "button",
|
|
1721
|
+
onClick: handleExportJson,
|
|
1722
|
+
disabled: !activeBlock,
|
|
1723
|
+
className: "bb-btn bb-btn--secondary"
|
|
1724
|
+
},
|
|
1725
|
+
"Export JSON"
|
|
1726
|
+
), /* @__PURE__ */ React8.createElement("div", { style: { width: 1, height: 24, background: "var(--bb-border)", margin: "0 4px" } }), /* @__PURE__ */ React8.createElement(
|
|
1727
|
+
"button",
|
|
1728
|
+
{
|
|
1729
|
+
type: "button",
|
|
1730
|
+
onClick: onTogglePreview,
|
|
1731
|
+
disabled: !activeBlock,
|
|
1732
|
+
className: `bb-btn ${previewOpen ? "bb-btn--primary" : "bb-btn--secondary"}`
|
|
1584
1733
|
},
|
|
1585
|
-
"
|
|
1586
|
-
), isReadOnly ? /* @__PURE__ */
|
|
1734
|
+
previewOpen ? "Close Preview" : "Live Preview"
|
|
1735
|
+
), isReadOnly ? /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(
|
|
1587
1736
|
"button",
|
|
1588
1737
|
{
|
|
1589
1738
|
type: "button",
|
|
@@ -1594,7 +1743,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1594
1743
|
disabled: !currentVersion
|
|
1595
1744
|
},
|
|
1596
1745
|
"Back to current"
|
|
1597
|
-
), /* @__PURE__ */
|
|
1746
|
+
), /* @__PURE__ */ React8.createElement(
|
|
1598
1747
|
"button",
|
|
1599
1748
|
{
|
|
1600
1749
|
type: "button",
|
|
@@ -1606,7 +1755,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1606
1755
|
className: "bb-btn bb-btn--warning"
|
|
1607
1756
|
},
|
|
1608
1757
|
notification?.status === "publishing" ? "Restoring..." : "Restore as new version"
|
|
1609
|
-
)) : /* @__PURE__ */
|
|
1758
|
+
)) : /* @__PURE__ */ React8.createElement(
|
|
1610
1759
|
"button",
|
|
1611
1760
|
{
|
|
1612
1761
|
type: "button",
|
|
@@ -1615,11 +1764,11 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1615
1764
|
className: "bb-btn bb-btn--primary"
|
|
1616
1765
|
},
|
|
1617
1766
|
notification?.status === "publishing" ? "Publishing..." : "Publish to Payload"
|
|
1618
|
-
))), notification?.status === "publishing" && /* @__PURE__ */
|
|
1767
|
+
))), notification?.status === "publishing" && /* @__PURE__ */ React8.createElement("div", { className: "bb-notify bb-notify--publishing" }, /* @__PURE__ */ React8.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ React8.createElement("div", { className: "bb-notify__spinner" }), /* @__PURE__ */ React8.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ React8.createElement("p", { className: "bb-notify__title" }, isReadOnly ? "Restoring version..." : "Publishing to Payload..."), /* @__PURE__ */ React8.createElement("p", { className: "bb-notify__sub" }, "Validating schema and saving block definition.")))), notification?.status === "success" && /* @__PURE__ */ React8.createElement("div", { className: "bb-notify bb-notify--success" }, /* @__PURE__ */ React8.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ React8.createElement("span", { className: "bb-notify__icon" }, "OK"), /* @__PURE__ */ React8.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ React8.createElement("p", { className: "bb-notify__title" }, notification.msg), /* @__PURE__ */ React8.createElement("p", { className: "bb-notify__sub" }, "The block definition and version have been saved.")), /* @__PURE__ */ React8.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, /* @__PURE__ */ React8.createElement(X, { size: 12, strokeWidth: 2 })))), notification?.status === "error" && /* @__PURE__ */ React8.createElement("div", { className: "bb-notify bb-notify--error" }, /* @__PURE__ */ React8.createElement("div", { className: "bb-notify__box" }, /* @__PURE__ */ React8.createElement("span", { className: "bb-notify__icon" }, "!"), /* @__PURE__ */ React8.createElement("div", { className: "bb-notify__body" }, /* @__PURE__ */ React8.createElement("p", { className: "bb-notify__title" }, notification.title), /* @__PURE__ */ React8.createElement("p", { className: "bb-notify__sub" }, "Fix the following errors before publishing:"), /* @__PURE__ */ React8.createElement("ul", { className: "bb-notify__error-list" }, notification.errors.map((e, i) => /* @__PURE__ */ React8.createElement("li", { key: i }, e)))), /* @__PURE__ */ React8.createElement("button", { className: "bb-notify__close", onClick: () => setNotification(null) }, /* @__PURE__ */ React8.createElement(X, { size: 12, strokeWidth: 2 })))));
|
|
1619
1768
|
}
|
|
1620
1769
|
|
|
1621
1770
|
// src/block-builder/components/canvas/BlockList.tsx
|
|
1622
|
-
import
|
|
1771
|
+
import React9 from "react";
|
|
1623
1772
|
import { Copy, Trash2, Plus } from "lucide-react";
|
|
1624
1773
|
function BlockList({ blockDefs = [], activeSlug, onBlockSelect }) {
|
|
1625
1774
|
const blocks = useBuilderStore((s) => s.blocks);
|
|
@@ -1633,37 +1782,37 @@ function BlockList({ blockDefs = [], activeSlug, onBlockSelect }) {
|
|
|
1633
1782
|
const localOnlyBlocks = blocks.filter(
|
|
1634
1783
|
(b) => !blockDefs.some((d) => d.slug === b.slug)
|
|
1635
1784
|
);
|
|
1636
|
-
return /* @__PURE__ */
|
|
1785
|
+
return /* @__PURE__ */ React9.createElement("div", { className: "bb-sidebar bb-sidebar--200 bb-sidebar--blocks" }, /* @__PURE__ */ React9.createElement("div", { className: "bb-sidebar__header" }, /* @__PURE__ */ React9.createElement("span", { className: "bb-sidebar__title" }, "Blocks"), /* @__PURE__ */ React9.createElement("button", { type: "button", onClick: addBlock, title: "Add block", className: "bb-sidebar__add" }, /* @__PURE__ */ React9.createElement(Plus, { size: 14, strokeWidth: 2 }))), /* @__PURE__ */ React9.createElement("div", { className: "bb-sidebar__body" }, useApiNav && blockDefs.map((def) => {
|
|
1637
1786
|
const isActive = def.slug === activeSlug;
|
|
1638
1787
|
const fieldCount = isActive && loadedBlock ? loadedBlock.fields.length : null;
|
|
1639
|
-
return /* @__PURE__ */
|
|
1788
|
+
return /* @__PURE__ */ React9.createElement(
|
|
1640
1789
|
"div",
|
|
1641
1790
|
{
|
|
1642
1791
|
key: def.id,
|
|
1643
1792
|
onClick: () => onBlockSelect?.(def.slug),
|
|
1644
1793
|
className: `bb-block-item${isActive ? " bb-block-item--active" : ""}`
|
|
1645
1794
|
},
|
|
1646
|
-
/* @__PURE__ */
|
|
1647
|
-
/* @__PURE__ */
|
|
1795
|
+
/* @__PURE__ */ React9.createElement("div", { className: "bb-block-item__slug" }, def.slug),
|
|
1796
|
+
/* @__PURE__ */ React9.createElement("div", { className: "bb-block-item__meta" }, fieldCount !== null ? `${fieldCount} field${fieldCount !== 1 ? "s" : ""}` : def.name)
|
|
1648
1797
|
);
|
|
1649
1798
|
}), localOnlyBlocks.map((block) => {
|
|
1650
1799
|
const isActive = block.id === activeBlockId;
|
|
1651
|
-
return /* @__PURE__ */
|
|
1800
|
+
return /* @__PURE__ */ React9.createElement(
|
|
1652
1801
|
"div",
|
|
1653
1802
|
{
|
|
1654
1803
|
key: block.id,
|
|
1655
1804
|
onClick: () => setActiveBlock(block.id),
|
|
1656
1805
|
className: `bb-block-item${isActive ? " bb-block-item--active" : ""}`
|
|
1657
1806
|
},
|
|
1658
|
-
/* @__PURE__ */
|
|
1659
|
-
/* @__PURE__ */
|
|
1660
|
-
/* @__PURE__ */
|
|
1807
|
+
/* @__PURE__ */ React9.createElement("div", { className: "bb-block-item__slug" }, block.slug),
|
|
1808
|
+
/* @__PURE__ */ React9.createElement("div", { className: "bb-block-item__meta" }, block.fields.length, " field", block.fields.length !== 1 ? "s" : ""),
|
|
1809
|
+
/* @__PURE__ */ React9.createElement(
|
|
1661
1810
|
"div",
|
|
1662
1811
|
{
|
|
1663
1812
|
className: "bb-block-item__actions",
|
|
1664
1813
|
onClick: (e) => e.stopPropagation()
|
|
1665
1814
|
},
|
|
1666
|
-
/* @__PURE__ */
|
|
1815
|
+
/* @__PURE__ */ React9.createElement(
|
|
1667
1816
|
"button",
|
|
1668
1817
|
{
|
|
1669
1818
|
type: "button",
|
|
@@ -1671,9 +1820,9 @@ function BlockList({ blockDefs = [], activeSlug, onBlockSelect }) {
|
|
|
1671
1820
|
className: "bb-block-action",
|
|
1672
1821
|
title: "Duplicate"
|
|
1673
1822
|
},
|
|
1674
|
-
/* @__PURE__ */
|
|
1823
|
+
/* @__PURE__ */ React9.createElement(Copy, { size: 12, strokeWidth: 1.75 })
|
|
1675
1824
|
),
|
|
1676
|
-
/* @__PURE__ */
|
|
1825
|
+
/* @__PURE__ */ React9.createElement(
|
|
1677
1826
|
"button",
|
|
1678
1827
|
{
|
|
1679
1828
|
type: "button",
|
|
@@ -1681,15 +1830,15 @@ function BlockList({ blockDefs = [], activeSlug, onBlockSelect }) {
|
|
|
1681
1830
|
className: "bb-block-action bb-block-action--danger",
|
|
1682
1831
|
title: "Delete"
|
|
1683
1832
|
},
|
|
1684
|
-
/* @__PURE__ */
|
|
1833
|
+
/* @__PURE__ */ React9.createElement(Trash2, { size: 12, strokeWidth: 1.75 })
|
|
1685
1834
|
)
|
|
1686
1835
|
)
|
|
1687
1836
|
);
|
|
1688
|
-
}), !useApiNav && blocks.length === 0 && /* @__PURE__ */
|
|
1837
|
+
}), !useApiNav && blocks.length === 0 && /* @__PURE__ */ React9.createElement("div", { className: "bb-block-empty" }, "No blocks yet.", /* @__PURE__ */ React9.createElement("br", null), "Click + to create one.")));
|
|
1689
1838
|
}
|
|
1690
1839
|
|
|
1691
1840
|
// src/block-builder/components/canvas/BuilderCanvas.tsx
|
|
1692
|
-
import
|
|
1841
|
+
import React11 from "react";
|
|
1693
1842
|
import {
|
|
1694
1843
|
DndContext,
|
|
1695
1844
|
closestCenter,
|
|
@@ -1706,68 +1855,107 @@ import {
|
|
|
1706
1855
|
import { restrictToVerticalAxis, restrictToParentElement } from "@dnd-kit/modifiers";
|
|
1707
1856
|
|
|
1708
1857
|
// src/block-builder/components/canvas/SortableFieldCard.tsx
|
|
1709
|
-
import
|
|
1858
|
+
import React10 from "react";
|
|
1710
1859
|
import { useSortable } from "@dnd-kit/sortable";
|
|
1711
1860
|
import { CSS } from "@dnd-kit/utilities";
|
|
1712
1861
|
import {
|
|
1713
1862
|
Type,
|
|
1714
1863
|
AlignLeft,
|
|
1864
|
+
AlignJustify,
|
|
1715
1865
|
Hash,
|
|
1716
1866
|
Mail,
|
|
1717
1867
|
Calendar,
|
|
1718
1868
|
CheckSquare,
|
|
1719
1869
|
ChevronDown as ChevronDown2,
|
|
1720
|
-
Circle,
|
|
1721
1870
|
Upload,
|
|
1871
|
+
Image,
|
|
1722
1872
|
Link,
|
|
1723
1873
|
Braces,
|
|
1874
|
+
List,
|
|
1875
|
+
Box,
|
|
1876
|
+
Columns,
|
|
1877
|
+
Folder,
|
|
1724
1878
|
X as X2
|
|
1725
1879
|
} from "lucide-react";
|
|
1726
1880
|
var ICON_MAP = {
|
|
1727
1881
|
text: Type,
|
|
1728
1882
|
textarea: AlignLeft,
|
|
1883
|
+
richtext: AlignJustify,
|
|
1729
1884
|
number: Hash,
|
|
1730
1885
|
email: Mail,
|
|
1731
1886
|
date: Calendar,
|
|
1732
1887
|
checkbox: CheckSquare,
|
|
1733
1888
|
select: ChevronDown2,
|
|
1734
|
-
|
|
1735
|
-
|
|
1889
|
+
image: Image,
|
|
1890
|
+
file: Upload,
|
|
1736
1891
|
relationship: Link,
|
|
1737
|
-
json: Braces
|
|
1892
|
+
json: Braces,
|
|
1893
|
+
array: List,
|
|
1894
|
+
group: Box,
|
|
1895
|
+
row: Columns,
|
|
1896
|
+
tabs: Folder,
|
|
1897
|
+
collapsible: ChevronDown2
|
|
1738
1898
|
};
|
|
1739
|
-
|
|
1899
|
+
var SortableFieldCard = React10.memo(function SortableFieldCard2({
|
|
1900
|
+
field,
|
|
1901
|
+
blockId,
|
|
1902
|
+
index,
|
|
1903
|
+
isActive,
|
|
1904
|
+
isReadOnly,
|
|
1905
|
+
onDrillDown
|
|
1906
|
+
}) {
|
|
1740
1907
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
1741
1908
|
id: field.id
|
|
1742
1909
|
});
|
|
1743
|
-
const activeFieldId = useBuilderStore((s) => s.activeFieldId);
|
|
1744
1910
|
const setActiveField = useBuilderStore((s) => s.setActiveField);
|
|
1745
1911
|
const removeField = useBuilderStore((s) => s.removeField);
|
|
1746
|
-
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
1747
|
-
const isActive = activeFieldId === field.id;
|
|
1748
1912
|
const wrapStyle = {
|
|
1749
1913
|
transform: CSS.Transform.toString(transform),
|
|
1750
1914
|
transition
|
|
1751
1915
|
};
|
|
1752
|
-
return /* @__PURE__ */
|
|
1916
|
+
return /* @__PURE__ */ React10.createElement(
|
|
1753
1917
|
"div",
|
|
1754
1918
|
{
|
|
1755
1919
|
ref: setNodeRef,
|
|
1756
1920
|
style: wrapStyle,
|
|
1757
1921
|
className: `bb-field-card-wrap${isDragging ? " bb-field-card-wrap--dragging" : ""}`,
|
|
1758
1922
|
...attributes,
|
|
1759
|
-
...listeners
|
|
1923
|
+
...listeners,
|
|
1924
|
+
"aria-label": `Draggable field card for ${field.name || "unnamed"}`
|
|
1760
1925
|
},
|
|
1761
|
-
/* @__PURE__ */
|
|
1926
|
+
/* @__PURE__ */ React10.createElement(
|
|
1762
1927
|
"div",
|
|
1763
1928
|
{
|
|
1764
1929
|
className: `bb-field-card${isActive ? " bb-field-card--active" : ""}`,
|
|
1765
1930
|
onClick: () => setActiveField(isActive ? null : field.id)
|
|
1766
1931
|
},
|
|
1767
|
-
/* @__PURE__ */
|
|
1768
|
-
/* @__PURE__ */
|
|
1769
|
-
/* @__PURE__ */
|
|
1770
|
-
|
|
1932
|
+
/* @__PURE__ */ React10.createElement("span", { className: "bb-field-card__icon" }, /* @__PURE__ */ React10.createElement(FieldIcon, { type: field.type })),
|
|
1933
|
+
/* @__PURE__ */ React10.createElement("div", { className: "bb-field-card__body" }, /* @__PURE__ */ React10.createElement("div", { className: "bb-field-card__name" }, field.name || /* @__PURE__ */ React10.createElement("span", { className: "bb-field-card__name--empty" }, "unnamed")), /* @__PURE__ */ React10.createElement("div", { className: "bb-field-card__type" }, field.type, field.required && /* @__PURE__ */ React10.createElement("span", { className: "bb-field-card__required" }, "*"))),
|
|
1934
|
+
/* @__PURE__ */ React10.createElement("span", { className: "bb-field-card__index" }, "#", index + 1),
|
|
1935
|
+
onDrillDown && /* @__PURE__ */ React10.createElement(
|
|
1936
|
+
"button",
|
|
1937
|
+
{
|
|
1938
|
+
type: "button",
|
|
1939
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
1940
|
+
onClick: (e) => {
|
|
1941
|
+
e.stopPropagation();
|
|
1942
|
+
onDrillDown();
|
|
1943
|
+
},
|
|
1944
|
+
className: "bb-field-card__drill",
|
|
1945
|
+
title: "Edit Inner Fields",
|
|
1946
|
+
style: {
|
|
1947
|
+
marginLeft: "auto",
|
|
1948
|
+
fontSize: 12,
|
|
1949
|
+
padding: "2px 8px",
|
|
1950
|
+
borderRadius: 4,
|
|
1951
|
+
border: "1px solid var(--theme-border)",
|
|
1952
|
+
background: "var(--theme-elevation-100)",
|
|
1953
|
+
cursor: "pointer"
|
|
1954
|
+
}
|
|
1955
|
+
},
|
|
1956
|
+
"Edit Fields"
|
|
1957
|
+
),
|
|
1958
|
+
!isReadOnly && /* @__PURE__ */ React10.createElement(
|
|
1771
1959
|
"button",
|
|
1772
1960
|
{
|
|
1773
1961
|
type: "button",
|
|
@@ -1779,21 +1967,46 @@ function SortableFieldCard({ field, blockId, index }) {
|
|
|
1779
1967
|
className: "bb-field-card__delete",
|
|
1780
1968
|
title: "Remove field"
|
|
1781
1969
|
},
|
|
1782
|
-
/* @__PURE__ */
|
|
1970
|
+
/* @__PURE__ */ React10.createElement(X2, { size: 12, strokeWidth: 2 })
|
|
1783
1971
|
)
|
|
1784
1972
|
)
|
|
1785
1973
|
);
|
|
1786
|
-
}
|
|
1974
|
+
});
|
|
1787
1975
|
function FieldIcon({ type }) {
|
|
1788
1976
|
const Icon = ICON_MAP[type];
|
|
1789
|
-
return Icon ? /* @__PURE__ */
|
|
1977
|
+
return Icon ? /* @__PURE__ */ React10.createElement(Icon, { size: 13, strokeWidth: 1.75 }) : null;
|
|
1790
1978
|
}
|
|
1791
1979
|
|
|
1792
1980
|
// src/block-builder/components/canvas/BuilderCanvas.tsx
|
|
1981
|
+
function resolvePathLabels(block, parentPath) {
|
|
1982
|
+
const labels = [];
|
|
1983
|
+
let currentFields = block.fields;
|
|
1984
|
+
for (const segment of parentPath) {
|
|
1985
|
+
const sepIndex = segment.indexOf("::");
|
|
1986
|
+
if (sepIndex !== -1) {
|
|
1987
|
+
const fieldId = segment.slice(0, sepIndex);
|
|
1988
|
+
const tabIndex = Number(segment.slice(sepIndex + 2));
|
|
1989
|
+
const tabsField = currentFields?.find((f) => f.id === fieldId);
|
|
1990
|
+
const tab = tabsField?.tabs?.[tabIndex];
|
|
1991
|
+
labels.push(tab?.label || tabsField?.name || "Tab");
|
|
1992
|
+
currentFields = tab?.fields;
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
const field = currentFields?.find((f) => f.id === segment);
|
|
1996
|
+
labels.push(field?.name || field?.label || "Nested Field");
|
|
1997
|
+
currentFields = field?.fields;
|
|
1998
|
+
}
|
|
1999
|
+
return labels;
|
|
2000
|
+
}
|
|
1793
2001
|
function BuilderCanvas() {
|
|
1794
2002
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1795
2003
|
const block = useBuilderStore((s) => s.blocks.find((b) => b.id === activeBlockId));
|
|
1796
2004
|
const reorderFields = useBuilderStore((s) => s.reorderFields);
|
|
2005
|
+
const activeFieldId = useBuilderStore((s) => s.activeFieldId);
|
|
2006
|
+
const activeParentPath = useBuilderStore((s) => s.activeParentPath);
|
|
2007
|
+
const truncateParentPath = useBuilderStore((s) => s.truncateParentPath);
|
|
2008
|
+
const resetParentPath = useBuilderStore((s) => s.resetParentPath);
|
|
2009
|
+
const pushParentPath = useBuilderStore((s) => s.pushParentPath);
|
|
1797
2010
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
1798
2011
|
const sensors = useSensors(
|
|
1799
2012
|
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
@@ -1803,16 +2016,41 @@ function BuilderCanvas() {
|
|
|
1803
2016
|
if (isReadOnly) return;
|
|
1804
2017
|
const { active, over } = event;
|
|
1805
2018
|
if (!over || active.id === over.id || !block) return;
|
|
1806
|
-
const
|
|
1807
|
-
|
|
2019
|
+
const targetFields2 = getTargetFields(block, activeParentPath);
|
|
2020
|
+
if (!targetFields2) return;
|
|
2021
|
+
const fromIndex = targetFields2.findIndex((f) => f.id === active.id);
|
|
2022
|
+
const toIndex = targetFields2.findIndex((f) => f.id === over.id);
|
|
1808
2023
|
if (fromIndex !== -1 && toIndex !== -1) {
|
|
1809
2024
|
reorderFields(block.id, fromIndex, toIndex);
|
|
1810
2025
|
}
|
|
1811
2026
|
}
|
|
1812
2027
|
if (!block) {
|
|
1813
|
-
return /* @__PURE__ */
|
|
2028
|
+
return /* @__PURE__ */ React11.createElement("div", { className: "bb-canvas", style: { display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React11.createElement("span", { className: "bb-canvas__no-block" }, "Select or create a block from the left panel."));
|
|
1814
2029
|
}
|
|
1815
|
-
|
|
2030
|
+
const targetFields = getTargetFields(block, activeParentPath) || [];
|
|
2031
|
+
const pathLabels = resolvePathLabels(block, activeParentPath);
|
|
2032
|
+
return /* @__PURE__ */ React11.createElement("div", { className: `bb-canvas${isReadOnly ? " bb-canvas--readonly" : ""}` }, /* @__PURE__ */ React11.createElement("div", { className: "bb-canvas__inner" }, /* @__PURE__ */ React11.createElement("div", { className: "bb-canvas__header" }, activeParentPath.length === 0 ? /* @__PURE__ */ React11.createElement(React11.Fragment, null, block.slug, " - ", targetFields.length, " field", targetFields.length !== 1 ? "s" : "") : /* @__PURE__ */ React11.createElement("nav", { className: "bb-breadcrumb", "aria-label": "Field path" }, /* @__PURE__ */ React11.createElement(
|
|
2033
|
+
"button",
|
|
2034
|
+
{
|
|
2035
|
+
type: "button",
|
|
2036
|
+
onClick: () => resetParentPath(),
|
|
2037
|
+
className: "bb-breadcrumb__item"
|
|
2038
|
+
},
|
|
2039
|
+
block.slug
|
|
2040
|
+
), /* @__PURE__ */ React11.createElement("span", { className: "bb-breadcrumb__sep" }, "/"), activeParentPath.map((segment, index) => {
|
|
2041
|
+
const isLast = index === activeParentPath.length - 1;
|
|
2042
|
+
return /* @__PURE__ */ React11.createElement(React11.Fragment, { key: segment }, /* @__PURE__ */ React11.createElement(
|
|
2043
|
+
"button",
|
|
2044
|
+
{
|
|
2045
|
+
type: "button",
|
|
2046
|
+
onClick: isLast ? void 0 : () => truncateParentPath(index + 1),
|
|
2047
|
+
className: "bb-breadcrumb__item",
|
|
2048
|
+
"aria-current": isLast ? "location" : void 0,
|
|
2049
|
+
"data-current": isLast ? "true" : void 0
|
|
2050
|
+
},
|
|
2051
|
+
pathLabels[index]
|
|
2052
|
+
), !isLast && /* @__PURE__ */ React11.createElement("span", { className: "bb-breadcrumb__sep" }, "/"));
|
|
2053
|
+
}))), targetFields.length === 0 ? /* @__PURE__ */ React11.createElement("div", { className: "bb-canvas__empty" }, "Add fields from the palette on the left") : /* @__PURE__ */ React11.createElement(
|
|
1816
2054
|
DndContext,
|
|
1817
2055
|
{
|
|
1818
2056
|
sensors,
|
|
@@ -1820,19 +2058,22 @@ function BuilderCanvas() {
|
|
|
1820
2058
|
modifiers: [restrictToVerticalAxis, restrictToParentElement],
|
|
1821
2059
|
onDragEnd: handleDragEnd
|
|
1822
2060
|
},
|
|
1823
|
-
/* @__PURE__ */
|
|
2061
|
+
/* @__PURE__ */ React11.createElement(
|
|
1824
2062
|
SortableContext,
|
|
1825
2063
|
{
|
|
1826
|
-
items:
|
|
2064
|
+
items: targetFields.map((f) => f.id),
|
|
1827
2065
|
strategy: verticalListSortingStrategy
|
|
1828
2066
|
},
|
|
1829
|
-
/* @__PURE__ */
|
|
2067
|
+
/* @__PURE__ */ React11.createElement("div", { className: "bb-canvas__field-list" }, targetFields.map((field, i) => /* @__PURE__ */ React11.createElement(
|
|
1830
2068
|
SortableFieldCard,
|
|
1831
2069
|
{
|
|
1832
2070
|
key: field.id,
|
|
1833
2071
|
field,
|
|
1834
2072
|
blockId: block.id,
|
|
1835
|
-
index: i
|
|
2073
|
+
index: i,
|
|
2074
|
+
isActive: activeFieldId === field.id,
|
|
2075
|
+
isReadOnly,
|
|
2076
|
+
onDrillDown: ["group", "row", "array", "collapsible"].includes(field.type) ? () => pushParentPath(field.id) : void 0
|
|
1836
2077
|
}
|
|
1837
2078
|
)))
|
|
1838
2079
|
)
|
|
@@ -1840,18 +2081,18 @@ function BuilderCanvas() {
|
|
|
1840
2081
|
}
|
|
1841
2082
|
|
|
1842
2083
|
// src/block-builder/components/canvas/ConfigPanel.tsx
|
|
1843
|
-
import
|
|
2084
|
+
import React14, { useState as useState5 } from "react";
|
|
1844
2085
|
|
|
1845
2086
|
// src/block-builder/components/config/BlockConfig.tsx
|
|
1846
|
-
import
|
|
2087
|
+
import React12 from "react";
|
|
1847
2088
|
function BlockConfig() {
|
|
1848
2089
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1849
2090
|
const block = useBuilderStore((s) => s.blocks.find((b) => b.id === activeBlockId));
|
|
1850
2091
|
const updateBlock = useBuilderStore((s) => s.updateBlock);
|
|
1851
2092
|
if (!block) {
|
|
1852
|
-
return /* @__PURE__ */
|
|
2093
|
+
return /* @__PURE__ */ React12.createElement("div", { className: "bb-form__empty" }, "No block selected.");
|
|
1853
2094
|
}
|
|
1854
|
-
return /* @__PURE__ */
|
|
2095
|
+
return /* @__PURE__ */ React12.createElement("div", { className: "bb-form" }, /* @__PURE__ */ React12.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React12.createElement("label", { className: "bb-form__label" }, "Slug *"), /* @__PURE__ */ React12.createElement(
|
|
1855
2096
|
"input",
|
|
1856
2097
|
{
|
|
1857
2098
|
type: "text",
|
|
@@ -1860,7 +2101,7 @@ function BlockConfig() {
|
|
|
1860
2101
|
placeholder: "myBlock",
|
|
1861
2102
|
className: "bb-input"
|
|
1862
2103
|
}
|
|
1863
|
-
), /* @__PURE__ */
|
|
2104
|
+
), /* @__PURE__ */ React12.createElement("span", { className: "bb-form__hint" }, "Unique identifier used in code and database")), /* @__PURE__ */ React12.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React12.createElement("label", { className: "bb-form__label" }, "Interface Name"), /* @__PURE__ */ React12.createElement(
|
|
1864
2105
|
"input",
|
|
1865
2106
|
{
|
|
1866
2107
|
type: "text",
|
|
@@ -1869,7 +2110,7 @@ function BlockConfig() {
|
|
|
1869
2110
|
placeholder: "MyBlock",
|
|
1870
2111
|
className: "bb-input"
|
|
1871
2112
|
}
|
|
1872
|
-
)), /* @__PURE__ */
|
|
2113
|
+
)), /* @__PURE__ */ React12.createElement("div", { className: "bb-grid-2" }, /* @__PURE__ */ React12.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React12.createElement("label", { className: "bb-form__label" }, "Singular Label"), /* @__PURE__ */ React12.createElement(
|
|
1873
2114
|
"input",
|
|
1874
2115
|
{
|
|
1875
2116
|
type: "text",
|
|
@@ -1878,7 +2119,7 @@ function BlockConfig() {
|
|
|
1878
2119
|
placeholder: "My Block",
|
|
1879
2120
|
className: "bb-input"
|
|
1880
2121
|
}
|
|
1881
|
-
)), /* @__PURE__ */
|
|
2122
|
+
)), /* @__PURE__ */ React12.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React12.createElement("label", { className: "bb-form__label" }, "Plural Label"), /* @__PURE__ */ React12.createElement(
|
|
1882
2123
|
"input",
|
|
1883
2124
|
{
|
|
1884
2125
|
type: "text",
|
|
@@ -1887,7 +2128,7 @@ function BlockConfig() {
|
|
|
1887
2128
|
placeholder: "My Blocks",
|
|
1888
2129
|
className: "bb-input"
|
|
1889
2130
|
}
|
|
1890
|
-
))), /* @__PURE__ */
|
|
2131
|
+
))), /* @__PURE__ */ React12.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React12.createElement("label", { className: "bb-form__label" }, "Image URL"), /* @__PURE__ */ React12.createElement(
|
|
1891
2132
|
"input",
|
|
1892
2133
|
{
|
|
1893
2134
|
type: "text",
|
|
@@ -1896,38 +2137,62 @@ function BlockConfig() {
|
|
|
1896
2137
|
placeholder: "https://...",
|
|
1897
2138
|
className: "bb-input"
|
|
1898
2139
|
}
|
|
1899
|
-
)), /* @__PURE__ */
|
|
2140
|
+
)), /* @__PURE__ */ React12.createElement("div", { className: "bb-stat-box" }, /* @__PURE__ */ React12.createElement("strong", null, block.fields.length), " field", block.fields.length !== 1 ? "s" : "", " defined"));
|
|
1900
2141
|
}
|
|
1901
2142
|
|
|
1902
2143
|
// src/block-builder/components/config/FieldConfig.tsx
|
|
1903
|
-
import
|
|
2144
|
+
import React13 from "react";
|
|
1904
2145
|
var ALL_TYPES = [
|
|
1905
2146
|
"text",
|
|
1906
2147
|
"textarea",
|
|
2148
|
+
"richtext",
|
|
1907
2149
|
"number",
|
|
1908
2150
|
"email",
|
|
2151
|
+
"url",
|
|
2152
|
+
"color",
|
|
1909
2153
|
"date",
|
|
1910
2154
|
"checkbox",
|
|
1911
2155
|
"select",
|
|
1912
|
-
"
|
|
1913
|
-
"
|
|
2156
|
+
"multiselect",
|
|
2157
|
+
"image",
|
|
2158
|
+
"file",
|
|
1914
2159
|
"relationship",
|
|
1915
|
-
"json"
|
|
2160
|
+
"json",
|
|
2161
|
+
"array",
|
|
2162
|
+
"group",
|
|
2163
|
+
"blocks",
|
|
2164
|
+
"row",
|
|
2165
|
+
"tabs",
|
|
2166
|
+
"collapsible"
|
|
1916
2167
|
];
|
|
2168
|
+
var NO_DEFAULT_VALUE_TYPES = /* @__PURE__ */ new Set([
|
|
2169
|
+
"array",
|
|
2170
|
+
"group",
|
|
2171
|
+
"blocks",
|
|
2172
|
+
"row",
|
|
2173
|
+
"tabs",
|
|
2174
|
+
"collapsible",
|
|
2175
|
+
"image",
|
|
2176
|
+
"file",
|
|
2177
|
+
"relationship",
|
|
2178
|
+
"json"
|
|
2179
|
+
]);
|
|
1917
2180
|
function FieldConfig() {
|
|
1918
2181
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1919
2182
|
const activeFieldId = useBuilderStore((s) => s.activeFieldId);
|
|
2183
|
+
const activeParentPath = useBuilderStore((s) => s.activeParentPath);
|
|
1920
2184
|
const block = useBuilderStore((s) => s.blocks.find((b) => b.id === activeBlockId));
|
|
1921
|
-
const field = block?.
|
|
2185
|
+
const field = block ? getTargetFields(block, activeParentPath)?.find((f) => f.id === activeFieldId) : void 0;
|
|
1922
2186
|
const updateField = useBuilderStore((s) => s.updateField);
|
|
2187
|
+
const pushParentPath = useBuilderStore((s) => s.pushParentPath);
|
|
1923
2188
|
if (!activeBlockId || !activeFieldId || !field) {
|
|
1924
|
-
return /* @__PURE__ */
|
|
2189
|
+
return /* @__PURE__ */ React13.createElement("div", { className: "bb-form__empty" }, "Select a field to configure it.");
|
|
1925
2190
|
}
|
|
1926
2191
|
function upd(updates) {
|
|
1927
2192
|
updateField(activeBlockId, activeFieldId, updates);
|
|
1928
2193
|
}
|
|
1929
|
-
const needsOptions = field.type === "select" || field.type === "
|
|
1930
|
-
return /* @__PURE__ */
|
|
2194
|
+
const needsOptions = field.type === "select" || field.type === "multiselect";
|
|
2195
|
+
return /* @__PURE__ */ React13.createElement("div", { className: "bb-form" }, /* @__PURE__ */ React13.createElement("div", { className: "bb-grid-2" }, /* @__PURE__ */ React13.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React13.createElement("label", { className: "bb-form__label" }, "Field Name *"), /* @__PURE__ */ React13.createElement(
|
|
1931
2196
|
"input",
|
|
1932
2197
|
{
|
|
1933
2198
|
type: "text",
|
|
@@ -1936,15 +2201,15 @@ function FieldConfig() {
|
|
|
1936
2201
|
placeholder: "fieldName",
|
|
1937
2202
|
className: "bb-input"
|
|
1938
2203
|
}
|
|
1939
|
-
)), /* @__PURE__ */
|
|
2204
|
+
)), /* @__PURE__ */ React13.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React13.createElement("label", { className: "bb-form__label" }, "Type *"), /* @__PURE__ */ React13.createElement(
|
|
1940
2205
|
"select",
|
|
1941
2206
|
{
|
|
1942
2207
|
value: field.type,
|
|
1943
2208
|
onChange: (e) => upd({ type: e.target.value }),
|
|
1944
2209
|
className: "bb-input bb-select"
|
|
1945
2210
|
},
|
|
1946
|
-
ALL_TYPES.map((t) => /* @__PURE__ */
|
|
1947
|
-
))), /* @__PURE__ */
|
|
2211
|
+
ALL_TYPES.map((t) => /* @__PURE__ */ React13.createElement("option", { key: t, value: t }, t))
|
|
2212
|
+
))), /* @__PURE__ */ React13.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React13.createElement("label", { className: "bb-form__label" }, "Label"), /* @__PURE__ */ React13.createElement(
|
|
1948
2213
|
"input",
|
|
1949
2214
|
{
|
|
1950
2215
|
type: "text",
|
|
@@ -1953,28 +2218,44 @@ function FieldConfig() {
|
|
|
1953
2218
|
placeholder: "Human-readable label",
|
|
1954
2219
|
className: "bb-input"
|
|
1955
2220
|
}
|
|
1956
|
-
)), /* @__PURE__ */
|
|
2221
|
+
)), /* @__PURE__ */ React13.createElement("div", { className: "bb-flags-row" }, /* @__PURE__ */ React13.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ React13.createElement(
|
|
1957
2222
|
"input",
|
|
1958
2223
|
{
|
|
1959
2224
|
type: "checkbox",
|
|
1960
2225
|
checked: field.required ?? false,
|
|
1961
2226
|
onChange: (e) => upd({ required: e.target.checked })
|
|
1962
2227
|
}
|
|
1963
|
-
), "Required"), /* @__PURE__ */
|
|
2228
|
+
), "Required"), /* @__PURE__ */ React13.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ React13.createElement(
|
|
1964
2229
|
"input",
|
|
1965
2230
|
{
|
|
1966
2231
|
type: "checkbox",
|
|
1967
2232
|
checked: field.unique ?? false,
|
|
1968
2233
|
onChange: (e) => upd({ unique: e.target.checked })
|
|
1969
2234
|
}
|
|
1970
|
-
), "Unique"), /* @__PURE__ */
|
|
2235
|
+
), "Unique"), /* @__PURE__ */ React13.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ React13.createElement(
|
|
1971
2236
|
"input",
|
|
1972
2237
|
{
|
|
1973
2238
|
type: "checkbox",
|
|
1974
2239
|
checked: field.localized ?? false,
|
|
1975
2240
|
onChange: (e) => upd({ localized: e.target.checked })
|
|
1976
2241
|
}
|
|
1977
|
-
), "Localized")), /* @__PURE__ */
|
|
2242
|
+
), "Localized")), !NO_DEFAULT_VALUE_TYPES.has(field.type) && /* @__PURE__ */ React13.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React13.createElement("label", { className: "bb-form__label" }, "Default Value"), field.type === "checkbox" ? /* @__PURE__ */ React13.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ React13.createElement(
|
|
2243
|
+
"input",
|
|
2244
|
+
{
|
|
2245
|
+
type: "checkbox",
|
|
2246
|
+
checked: field.defaultValue === true,
|
|
2247
|
+
onChange: (e) => upd({ defaultValue: e.target.checked })
|
|
2248
|
+
}
|
|
2249
|
+
), "Checked by default") : field.type === "number" ? /* @__PURE__ */ React13.createElement(
|
|
2250
|
+
"input",
|
|
2251
|
+
{
|
|
2252
|
+
type: "number",
|
|
2253
|
+
value: typeof field.defaultValue === "number" ? field.defaultValue : "",
|
|
2254
|
+
onChange: (e) => upd({ defaultValue: e.target.value === "" ? void 0 : e.target.valueAsNumber }),
|
|
2255
|
+
placeholder: "0",
|
|
2256
|
+
className: "bb-input"
|
|
2257
|
+
}
|
|
2258
|
+
) : /* @__PURE__ */ React13.createElement(
|
|
1978
2259
|
"input",
|
|
1979
2260
|
{
|
|
1980
2261
|
type: "text",
|
|
@@ -1983,23 +2264,23 @@ function FieldConfig() {
|
|
|
1983
2264
|
placeholder: "Default value",
|
|
1984
2265
|
className: "bb-input"
|
|
1985
2266
|
}
|
|
1986
|
-
)), field.type === "relationship" && /* @__PURE__ */
|
|
2267
|
+
)), field.type === "relationship" && /* @__PURE__ */ React13.createElement(React13.Fragment, null, /* @__PURE__ */ React13.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React13.createElement("label", { className: "bb-form__label" }, "Relation To (collection slug)"), /* @__PURE__ */ React13.createElement(
|
|
1987
2268
|
"input",
|
|
1988
2269
|
{
|
|
1989
2270
|
type: "text",
|
|
1990
|
-
value: field.
|
|
1991
|
-
onChange: (e) => upd({
|
|
2271
|
+
value: field.collection ?? "",
|
|
2272
|
+
onChange: (e) => upd({ collection: e.target.value }),
|
|
1992
2273
|
placeholder: "pages",
|
|
1993
2274
|
className: "bb-input"
|
|
1994
2275
|
}
|
|
1995
|
-
)), /* @__PURE__ */
|
|
2276
|
+
)), /* @__PURE__ */ React13.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ React13.createElement(
|
|
1996
2277
|
"input",
|
|
1997
2278
|
{
|
|
1998
2279
|
type: "checkbox",
|
|
1999
2280
|
checked: field.hasMany ?? false,
|
|
2000
2281
|
onChange: (e) => upd({ hasMany: e.target.checked })
|
|
2001
2282
|
}
|
|
2002
|
-
), "Has Many")), field.type === "array" && /* @__PURE__ */
|
|
2283
|
+
), "Has Many")), field.type === "array" && /* @__PURE__ */ React13.createElement("div", { className: "bb-grid-2" }, /* @__PURE__ */ React13.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React13.createElement("label", { className: "bb-form__label" }, "Min Rows"), /* @__PURE__ */ React13.createElement(
|
|
2003
2284
|
"input",
|
|
2004
2285
|
{
|
|
2005
2286
|
type: "number",
|
|
@@ -2007,7 +2288,7 @@ function FieldConfig() {
|
|
|
2007
2288
|
onChange: (e) => upd({ minRows: e.target.value === "" ? void 0 : e.target.valueAsNumber }),
|
|
2008
2289
|
className: "bb-input"
|
|
2009
2290
|
}
|
|
2010
|
-
)), /* @__PURE__ */
|
|
2291
|
+
)), /* @__PURE__ */ React13.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React13.createElement("label", { className: "bb-form__label" }, "Max Rows"), /* @__PURE__ */ React13.createElement(
|
|
2011
2292
|
"input",
|
|
2012
2293
|
{
|
|
2013
2294
|
type: "number",
|
|
@@ -2015,7 +2296,54 @@ function FieldConfig() {
|
|
|
2015
2296
|
onChange: (e) => upd({ maxRows: e.target.value === "" ? void 0 : e.target.valueAsNumber }),
|
|
2016
2297
|
className: "bb-input"
|
|
2017
2298
|
}
|
|
2018
|
-
))),
|
|
2299
|
+
))), field.type === "tabs" && /* @__PURE__ */ React13.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React13.createElement("div", { className: "bb-options-label" }, "Tabs"), (field.tabs ?? []).map((tab, i) => /* @__PURE__ */ React13.createElement("div", { key: tab.id ?? i, className: "bb-option-row" }, /* @__PURE__ */ React13.createElement(
|
|
2300
|
+
"input",
|
|
2301
|
+
{
|
|
2302
|
+
type: "text",
|
|
2303
|
+
value: tab.label,
|
|
2304
|
+
placeholder: "Tab label",
|
|
2305
|
+
onChange: (e) => {
|
|
2306
|
+
const next = [...field.tabs ?? []];
|
|
2307
|
+
next[i] = { ...next[i], label: e.target.value };
|
|
2308
|
+
upd({ tabs: next });
|
|
2309
|
+
},
|
|
2310
|
+
className: "bb-input"
|
|
2311
|
+
}
|
|
2312
|
+
), /* @__PURE__ */ React13.createElement(
|
|
2313
|
+
"button",
|
|
2314
|
+
{
|
|
2315
|
+
type: "button",
|
|
2316
|
+
onClick: () => pushParentPath(encodeTabPath(field.id, i)),
|
|
2317
|
+
className: "bb-add-option",
|
|
2318
|
+
title: "Edit this tab's fields"
|
|
2319
|
+
},
|
|
2320
|
+
"Edit Fields"
|
|
2321
|
+
), /* @__PURE__ */ React13.createElement(
|
|
2322
|
+
"button",
|
|
2323
|
+
{
|
|
2324
|
+
type: "button",
|
|
2325
|
+
onClick: () => {
|
|
2326
|
+
const next = (field.tabs ?? []).filter((_, k) => k !== i);
|
|
2327
|
+
upd({ tabs: next });
|
|
2328
|
+
},
|
|
2329
|
+
className: "bb-option-delete",
|
|
2330
|
+
title: "Remove tab"
|
|
2331
|
+
},
|
|
2332
|
+
"x"
|
|
2333
|
+
))), /* @__PURE__ */ React13.createElement(
|
|
2334
|
+
"button",
|
|
2335
|
+
{
|
|
2336
|
+
type: "button",
|
|
2337
|
+
onClick: () => upd({
|
|
2338
|
+
tabs: [
|
|
2339
|
+
...field.tabs ?? [],
|
|
2340
|
+
{ id: uuidv4(), label: `Tab ${(field.tabs?.length ?? 0) + 1}`, fields: [] }
|
|
2341
|
+
]
|
|
2342
|
+
}),
|
|
2343
|
+
className: "bb-add-option"
|
|
2344
|
+
},
|
|
2345
|
+
"+ Add Tab"
|
|
2346
|
+
)), needsOptions && /* @__PURE__ */ React13.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React13.createElement("div", { className: "bb-options-label" }, "Options"), (field.options ?? []).map((opt, i) => /* @__PURE__ */ React13.createElement("div", { key: i, className: "bb-option-row" }, /* @__PURE__ */ React13.createElement(
|
|
2019
2347
|
"input",
|
|
2020
2348
|
{
|
|
2021
2349
|
type: "text",
|
|
@@ -2028,7 +2356,7 @@ function FieldConfig() {
|
|
|
2028
2356
|
},
|
|
2029
2357
|
className: "bb-input"
|
|
2030
2358
|
}
|
|
2031
|
-
), /* @__PURE__ */
|
|
2359
|
+
), /* @__PURE__ */ React13.createElement(
|
|
2032
2360
|
"input",
|
|
2033
2361
|
{
|
|
2034
2362
|
type: "text",
|
|
@@ -2041,7 +2369,7 @@ function FieldConfig() {
|
|
|
2041
2369
|
},
|
|
2042
2370
|
className: "bb-input"
|
|
2043
2371
|
}
|
|
2044
|
-
), /* @__PURE__ */
|
|
2372
|
+
), /* @__PURE__ */ React13.createElement(
|
|
2045
2373
|
"button",
|
|
2046
2374
|
{
|
|
2047
2375
|
type: "button",
|
|
@@ -2053,7 +2381,7 @@ function FieldConfig() {
|
|
|
2053
2381
|
title: "Remove option"
|
|
2054
2382
|
},
|
|
2055
2383
|
"x"
|
|
2056
|
-
))), /* @__PURE__ */
|
|
2384
|
+
))), /* @__PURE__ */ React13.createElement(
|
|
2057
2385
|
"button",
|
|
2058
2386
|
{
|
|
2059
2387
|
type: "button",
|
|
@@ -2070,7 +2398,7 @@ function ConfigPanel() {
|
|
|
2070
2398
|
const activeFieldId = useBuilderStore((s) => s.activeFieldId);
|
|
2071
2399
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
2072
2400
|
const activeTab = activeFieldId ? "field" : tab;
|
|
2073
|
-
return /* @__PURE__ */
|
|
2401
|
+
return /* @__PURE__ */ React14.createElement("div", { className: `bb-config${isReadOnly ? " bb-config--readonly" : ""}` }, /* @__PURE__ */ React14.createElement("div", { className: "bb-config__tabs" }, ["block", "field"].map((t) => /* @__PURE__ */ React14.createElement(
|
|
2074
2402
|
"button",
|
|
2075
2403
|
{
|
|
2076
2404
|
key: t,
|
|
@@ -2079,26 +2407,29 @@ function ConfigPanel() {
|
|
|
2079
2407
|
className: `bb-config__tab${activeTab === t ? " bb-config__tab--active" : ""}`
|
|
2080
2408
|
},
|
|
2081
2409
|
t
|
|
2082
|
-
))), /* @__PURE__ */
|
|
2410
|
+
))), /* @__PURE__ */ React14.createElement("div", { className: "bb-config__body" }, activeTab === "block" ? /* @__PURE__ */ React14.createElement(BlockConfig, null) : /* @__PURE__ */ React14.createElement(FieldConfig, null)), isReadOnly && /* @__PURE__ */ React14.createElement("div", { className: "bb-config__readonly-overlay" }, /* @__PURE__ */ React14.createElement("span", { className: "bb-config__readonly-label" }, "Read only")));
|
|
2083
2411
|
}
|
|
2084
2412
|
|
|
2085
2413
|
// src/block-builder/components/sidebar/FieldPalette.tsx
|
|
2086
|
-
import
|
|
2414
|
+
import React15, { useState as useState6 } from "react";
|
|
2087
2415
|
import {
|
|
2088
2416
|
Type as Type2,
|
|
2089
2417
|
AlignLeft as AlignLeft2,
|
|
2090
|
-
AlignJustify,
|
|
2418
|
+
AlignJustify as AlignJustify2,
|
|
2091
2419
|
Hash as Hash2,
|
|
2092
2420
|
Mail as Mail2,
|
|
2093
2421
|
Calendar as Calendar2,
|
|
2094
2422
|
CheckSquare as CheckSquare2,
|
|
2095
2423
|
ChevronDown as ChevronDown3,
|
|
2096
|
-
Circle
|
|
2424
|
+
Circle,
|
|
2097
2425
|
Upload as Upload2,
|
|
2098
2426
|
Link as Link2,
|
|
2099
|
-
List,
|
|
2100
|
-
Folder,
|
|
2101
|
-
Braces as Braces2
|
|
2427
|
+
List as List2,
|
|
2428
|
+
Folder as Folder2,
|
|
2429
|
+
Braces as Braces2,
|
|
2430
|
+
Image as Image2,
|
|
2431
|
+
Box as Box2,
|
|
2432
|
+
Columns as Columns2
|
|
2102
2433
|
} from "lucide-react";
|
|
2103
2434
|
|
|
2104
2435
|
// src/block-builder/lib/field-palette.ts
|
|
@@ -2112,11 +2443,17 @@ var FIELD_PALETTE = [
|
|
|
2112
2443
|
{ type: "checkbox", label: "Checkbox", description: "Boolean toggle", icon: "CheckSquare", category: "basic", color: "bg-blue-500/20 text-blue-400 border-blue-500/30" },
|
|
2113
2444
|
// Choice
|
|
2114
2445
|
{ type: "select", label: "Select", description: "Dropdown selection", icon: "ChevronDown", category: "choice", color: "bg-purple-500/20 text-purple-400 border-purple-500/30" },
|
|
2115
|
-
{ type: "radio", label: "Radio", description: "Radio button group", icon: "Circle", category: "choice", color: "bg-purple-500/20 text-purple-400 border-purple-500/30" },
|
|
2116
2446
|
// Media
|
|
2117
|
-
{ type: "
|
|
2447
|
+
{ type: "image", label: "Image", description: "Image upload", icon: "Image", category: "media", color: "bg-green-500/20 text-green-400 border-green-500/30" },
|
|
2448
|
+
{ type: "file", label: "File", description: "File upload", icon: "Upload", category: "media", color: "bg-green-500/20 text-green-400 border-green-500/30" },
|
|
2118
2449
|
// Relational
|
|
2119
2450
|
{ type: "relationship", label: "Relationship", description: "Link to another collection", icon: "Link", category: "relational", color: "bg-orange-500/20 text-orange-400 border-orange-500/30" },
|
|
2451
|
+
// Layout
|
|
2452
|
+
{ type: "array", label: "Array", description: "Repeating list of fields", icon: "List", category: "layout", color: "bg-pink-500/20 text-pink-400 border-pink-500/30" },
|
|
2453
|
+
{ type: "group", label: "Group", description: "Grouped fields object", icon: "Box", category: "layout", color: "bg-pink-500/20 text-pink-400 border-pink-500/30" },
|
|
2454
|
+
{ type: "row", label: "Row", description: "Horizontal layout row", icon: "Columns", category: "layout", color: "bg-pink-500/20 text-pink-400 border-pink-500/30" },
|
|
2455
|
+
{ type: "tabs", label: "Tabs", description: "Tabbed layout container", icon: "Folder", category: "layout", color: "bg-pink-500/20 text-pink-400 border-pink-500/30" },
|
|
2456
|
+
{ type: "collapsible", label: "Collapsible", description: "Expandable field group", icon: "ChevronDown", category: "layout", color: "bg-pink-500/20 text-pink-400 border-pink-500/30" },
|
|
2120
2457
|
// Advanced
|
|
2121
2458
|
{ type: "json", label: "JSON", description: "Raw JSON data field", icon: "Braces", category: "advanced", color: "bg-red-500/20 text-red-400 border-red-500/30" }
|
|
2122
2459
|
];
|
|
@@ -2125,6 +2462,7 @@ var FIELD_CATEGORIES = [
|
|
|
2125
2462
|
{ id: "choice", label: "Choice" },
|
|
2126
2463
|
{ id: "media", label: "Media" },
|
|
2127
2464
|
{ id: "relational", label: "Relational" },
|
|
2465
|
+
{ id: "layout", label: "Layout" },
|
|
2128
2466
|
{ id: "advanced", label: "Advanced" }
|
|
2129
2467
|
];
|
|
2130
2468
|
function getFieldMeta(type) {
|
|
@@ -2135,18 +2473,21 @@ function getFieldMeta(type) {
|
|
|
2135
2473
|
var ICON_MAP2 = {
|
|
2136
2474
|
Type: Type2,
|
|
2137
2475
|
AlignLeft: AlignLeft2,
|
|
2138
|
-
AlignJustify,
|
|
2476
|
+
AlignJustify: AlignJustify2,
|
|
2139
2477
|
Hash: Hash2,
|
|
2140
2478
|
Mail: Mail2,
|
|
2141
2479
|
Calendar: Calendar2,
|
|
2142
2480
|
CheckSquare: CheckSquare2,
|
|
2143
2481
|
ChevronDown: ChevronDown3,
|
|
2144
|
-
Circle
|
|
2482
|
+
Circle,
|
|
2145
2483
|
Upload: Upload2,
|
|
2146
2484
|
Link: Link2,
|
|
2147
|
-
List,
|
|
2148
|
-
Folder,
|
|
2149
|
-
Braces: Braces2
|
|
2485
|
+
List: List2,
|
|
2486
|
+
Folder: Folder2,
|
|
2487
|
+
Braces: Braces2,
|
|
2488
|
+
Image: Image2,
|
|
2489
|
+
Box: Box2,
|
|
2490
|
+
Columns: Columns2
|
|
2150
2491
|
};
|
|
2151
2492
|
function FieldPalette() {
|
|
2152
2493
|
const [search, setSearch] = useState6("");
|
|
@@ -2160,7 +2501,7 @@ function FieldPalette() {
|
|
|
2160
2501
|
if (!activeBlockId || isReadOnly) return;
|
|
2161
2502
|
addField(activeBlockId, type);
|
|
2162
2503
|
}
|
|
2163
|
-
return /* @__PURE__ */
|
|
2504
|
+
return /* @__PURE__ */ React15.createElement("div", { className: `bb-sidebar bb-sidebar--200 bb-sidebar--palette${isReadOnly ? " bb-sidebar--readonly" : ""}` }, /* @__PURE__ */ React15.createElement("div", { className: "bb-sidebar__header" }, /* @__PURE__ */ React15.createElement("span", { className: "bb-sidebar__title" }, "Fields")), /* @__PURE__ */ React15.createElement("div", { className: "bb-palette__search-wrap" }, /* @__PURE__ */ React15.createElement(
|
|
2164
2505
|
"input",
|
|
2165
2506
|
{
|
|
2166
2507
|
type: "text",
|
|
@@ -2169,7 +2510,7 @@ function FieldPalette() {
|
|
|
2169
2510
|
placeholder: "Search...",
|
|
2170
2511
|
className: "bb-palette__search"
|
|
2171
2512
|
}
|
|
2172
|
-
)), /* @__PURE__ */
|
|
2513
|
+
)), /* @__PURE__ */ React15.createElement("div", { className: "bb-sidebar__body" }, search.trim() ? /* @__PURE__ */ React15.createElement("div", { className: "bb-palette__items" }, filtered.map((item) => /* @__PURE__ */ React15.createElement(
|
|
2173
2514
|
FieldButton,
|
|
2174
2515
|
{
|
|
2175
2516
|
key: item.type,
|
|
@@ -2181,7 +2522,7 @@ function FieldPalette() {
|
|
|
2181
2522
|
}
|
|
2182
2523
|
))) : FIELD_CATEGORIES.map((cat) => {
|
|
2183
2524
|
const items = FIELD_PALETTE.filter((f) => f.category === cat.id);
|
|
2184
|
-
return /* @__PURE__ */
|
|
2525
|
+
return /* @__PURE__ */ React15.createElement("div", { key: cat.id, style: { marginTop: 6 } }, /* @__PURE__ */ React15.createElement("div", { className: "bb-palette__cat-label" }, cat.label), /* @__PURE__ */ React15.createElement("div", { className: "bb-palette__items" }, items.map((item) => /* @__PURE__ */ React15.createElement(
|
|
2185
2526
|
FieldButton,
|
|
2186
2527
|
{
|
|
2187
2528
|
key: item.type,
|
|
@@ -2192,7 +2533,7 @@ function FieldPalette() {
|
|
|
2192
2533
|
disabled: !activeBlockId || isReadOnly
|
|
2193
2534
|
}
|
|
2194
2535
|
))));
|
|
2195
|
-
})), !activeBlockId && /* @__PURE__ */
|
|
2536
|
+
})), !activeBlockId && /* @__PURE__ */ React15.createElement("div", { className: "bb-sidebar__footer" }, "Select a block first"));
|
|
2196
2537
|
}
|
|
2197
2538
|
function FieldButton({
|
|
2198
2539
|
type,
|
|
@@ -2203,7 +2544,7 @@ function FieldButton({
|
|
|
2203
2544
|
}) {
|
|
2204
2545
|
const meta = getFieldMeta(type);
|
|
2205
2546
|
const Icon = ICON_MAP2[icon];
|
|
2206
|
-
return /* @__PURE__ */
|
|
2547
|
+
return /* @__PURE__ */ React15.createElement(
|
|
2207
2548
|
"button",
|
|
2208
2549
|
{
|
|
2209
2550
|
type: "button",
|
|
@@ -2212,14 +2553,291 @@ function FieldButton({
|
|
|
2212
2553
|
title: meta?.description,
|
|
2213
2554
|
className: "bb-palette__item"
|
|
2214
2555
|
},
|
|
2215
|
-
/* @__PURE__ */
|
|
2216
|
-
/* @__PURE__ */
|
|
2217
|
-
/* @__PURE__ */
|
|
2556
|
+
/* @__PURE__ */ React15.createElement("span", { className: "bb-palette__item__icon" }, Icon ? /* @__PURE__ */ React15.createElement(Icon, { size: 13, strokeWidth: 1.75 }) : null),
|
|
2557
|
+
/* @__PURE__ */ React15.createElement("span", { className: "bb-palette__item__label" }, label),
|
|
2558
|
+
/* @__PURE__ */ React15.createElement("span", { className: "bb-palette__item__type" }, type)
|
|
2218
2559
|
);
|
|
2219
2560
|
}
|
|
2220
2561
|
|
|
2221
2562
|
// src/block-builder/components/canvas/CodePreview.tsx
|
|
2222
|
-
import
|
|
2563
|
+
import React16, { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef5, useState as useState7 } from "react";
|
|
2564
|
+
|
|
2565
|
+
// src/block-builder/lib/codegen.ts
|
|
2566
|
+
function indent(n) {
|
|
2567
|
+
return " ".repeat(n);
|
|
2568
|
+
}
|
|
2569
|
+
function safeStr(s) {
|
|
2570
|
+
return JSON.stringify(s);
|
|
2571
|
+
}
|
|
2572
|
+
function listToCode(items, body, depth) {
|
|
2573
|
+
if (items.length === 0) return "[]";
|
|
2574
|
+
const pad = indent(depth);
|
|
2575
|
+
const innerPad = indent(depth + 1);
|
|
2576
|
+
const entries = items.map((item) => `${innerPad}{
|
|
2577
|
+
${body(item)}
|
|
2578
|
+
${innerPad}}`).join(",\n");
|
|
2579
|
+
return `[
|
|
2580
|
+
${entries}
|
|
2581
|
+
${pad}]`;
|
|
2582
|
+
}
|
|
2583
|
+
var UNNAMED_TYPES = /* @__PURE__ */ new Set(["row", "tabs", "collapsible"]);
|
|
2584
|
+
var NO_LABEL_TYPES = /* @__PURE__ */ new Set(["row"]);
|
|
2585
|
+
var NO_LOCALIZED_TYPES = /* @__PURE__ */ new Set(["row", "tabs", "collapsible"]);
|
|
2586
|
+
var NO_ADMIN_DESCRIPTION_TYPES = /* @__PURE__ */ new Set(["row", "tabs"]);
|
|
2587
|
+
var FIELDS_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
2588
|
+
"array",
|
|
2589
|
+
"group",
|
|
2590
|
+
"row",
|
|
2591
|
+
"collapsible"
|
|
2592
|
+
]);
|
|
2593
|
+
var PAYLOAD_TYPE = {
|
|
2594
|
+
richtext: "richText",
|
|
2595
|
+
image: "upload",
|
|
2596
|
+
file: "upload",
|
|
2597
|
+
multiselect: "select",
|
|
2598
|
+
url: "text",
|
|
2599
|
+
color: "text"
|
|
2600
|
+
};
|
|
2601
|
+
var UPLOAD_TYPES = /* @__PURE__ */ new Set(["image", "file"]);
|
|
2602
|
+
var DEFAULT_UPLOAD_COLLECTION = "media";
|
|
2603
|
+
function payloadType(type) {
|
|
2604
|
+
return PAYLOAD_TYPE[type] ?? type;
|
|
2605
|
+
}
|
|
2606
|
+
function fieldToCode(field, depth = 1) {
|
|
2607
|
+
const pad = indent(depth);
|
|
2608
|
+
const innerPad = indent(depth + 1);
|
|
2609
|
+
const lines = [];
|
|
2610
|
+
if (!UNNAMED_TYPES.has(field.type)) {
|
|
2611
|
+
lines.push(`${pad}name: ${safeStr(field.name)}`);
|
|
2612
|
+
}
|
|
2613
|
+
lines.push(`${pad}type: '${payloadType(field.type)}'`);
|
|
2614
|
+
if (field.label && !NO_LABEL_TYPES.has(field.type)) {
|
|
2615
|
+
lines.push(`${pad}label: ${safeStr(field.label)}`);
|
|
2616
|
+
}
|
|
2617
|
+
if (field.required) lines.push(`${pad}required: true`);
|
|
2618
|
+
if (field.unique) lines.push(`${pad}unique: true`);
|
|
2619
|
+
if (field.localized && !NO_LOCALIZED_TYPES.has(field.type)) {
|
|
2620
|
+
lines.push(`${pad}localized: true`);
|
|
2621
|
+
}
|
|
2622
|
+
if (field.defaultValue !== void 0) {
|
|
2623
|
+
const val = typeof field.defaultValue === "string" ? safeStr(String(field.defaultValue)) : field.defaultValue;
|
|
2624
|
+
lines.push(`${pad}defaultValue: ${val}`);
|
|
2625
|
+
}
|
|
2626
|
+
if (field.type === "richtext") {
|
|
2627
|
+
lines.push(`${pad}editor: lexicalEditor({})`);
|
|
2628
|
+
}
|
|
2629
|
+
if (field.options && field.options.length > 0) {
|
|
2630
|
+
const opts = field.options.map((o) => `{ label: ${safeStr(o.label)}, value: ${safeStr(o.value)} }`).join(`, `);
|
|
2631
|
+
lines.push(`${pad}options: [${opts}]`);
|
|
2632
|
+
}
|
|
2633
|
+
if (UPLOAD_TYPES.has(field.type)) {
|
|
2634
|
+
lines.push(
|
|
2635
|
+
`${pad}relationTo: ${safeStr(field.collection || DEFAULT_UPLOAD_COLLECTION)}`
|
|
2636
|
+
);
|
|
2637
|
+
} else if (field.collection) {
|
|
2638
|
+
lines.push(`${pad}relationTo: ${safeStr(field.collection)}`);
|
|
2639
|
+
}
|
|
2640
|
+
if (field.type === "multiselect") {
|
|
2641
|
+
lines.push(`${pad}hasMany: true`);
|
|
2642
|
+
} else if (field.hasMany !== void 0) {
|
|
2643
|
+
lines.push(`${pad}hasMany: ${field.hasMany}`);
|
|
2644
|
+
}
|
|
2645
|
+
if (field.minRows !== void 0) lines.push(`${pad}minRows: ${field.minRows}`);
|
|
2646
|
+
if (field.maxRows !== void 0) lines.push(`${pad}maxRows: ${field.maxRows}`);
|
|
2647
|
+
const children = field.fields ?? [];
|
|
2648
|
+
if (children.length > 0 || FIELDS_CONTAINER_TYPES.has(field.type)) {
|
|
2649
|
+
lines.push(
|
|
2650
|
+
`${pad}fields: ${listToCode(children, (f) => fieldToCode(f, depth + 2), depth)}`
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
if (field.type === "blocks") {
|
|
2654
|
+
lines.push(`${pad}blocks: []`);
|
|
2655
|
+
}
|
|
2656
|
+
if (field.type === "tabs") {
|
|
2657
|
+
const tabsCode = (field.tabs ?? []).map((tab) => {
|
|
2658
|
+
const tabPad = indent(depth + 2);
|
|
2659
|
+
const tabLines = [];
|
|
2660
|
+
if (tab.name) tabLines.push(`${tabPad}name: ${safeStr(tab.name)}`);
|
|
2661
|
+
tabLines.push(`${tabPad}label: ${safeStr(tab.label)}`);
|
|
2662
|
+
tabLines.push(
|
|
2663
|
+
`${tabPad}fields: ${listToCode(tab.fields ?? [], (f) => fieldToCode(f, depth + 4), depth + 2)}`
|
|
2664
|
+
);
|
|
2665
|
+
return `${innerPad}{
|
|
2666
|
+
${tabLines.join(",\n")}
|
|
2667
|
+
${innerPad}}`;
|
|
2668
|
+
}).join(",\n");
|
|
2669
|
+
lines.push(
|
|
2670
|
+
`${pad}tabs: ${(field.tabs ?? []).length > 0 ? `[
|
|
2671
|
+
${tabsCode}
|
|
2672
|
+
${pad}]` : "[]"}`
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
const adminParts = [];
|
|
2676
|
+
if (field.admin?.description && !NO_ADMIN_DESCRIPTION_TYPES.has(field.type))
|
|
2677
|
+
adminParts.push(`description: ${safeStr(field.admin.description)}`);
|
|
2678
|
+
if (field.admin?.placeholder)
|
|
2679
|
+
adminParts.push(`placeholder: ${safeStr(field.admin.placeholder)}`);
|
|
2680
|
+
if (field.admin?.readOnly) adminParts.push(`readOnly: true`);
|
|
2681
|
+
if (field.admin?.hidden) adminParts.push(`hidden: true`);
|
|
2682
|
+
if (adminParts.length > 0) {
|
|
2683
|
+
lines.push(`${pad}admin: { ${adminParts.join(", ")} }`);
|
|
2684
|
+
}
|
|
2685
|
+
return lines.join(",\n");
|
|
2686
|
+
}
|
|
2687
|
+
function generateBlockCode(block) {
|
|
2688
|
+
const hasRichText = containsRichText(block.fields);
|
|
2689
|
+
const imports = [`import type { Block } from 'payload'`];
|
|
2690
|
+
if (hasRichText) {
|
|
2691
|
+
imports.push(`import { lexicalEditor } from '@payloadcms/richtext-lexical'`);
|
|
2692
|
+
}
|
|
2693
|
+
const fieldsCode = block.fields.map((f) => ` {
|
|
2694
|
+
${fieldToCode(f, 3)}
|
|
2695
|
+
}`).join(",\n");
|
|
2696
|
+
const labelsCode = block.labels ? `
|
|
2697
|
+
labels: {
|
|
2698
|
+
singular: ${safeStr(block.labels.singular ?? block.slug)},
|
|
2699
|
+
plural: ${safeStr(block.labels.plural ?? block.slug + "s")},
|
|
2700
|
+
},` : "";
|
|
2701
|
+
const interfaceLine = block.interfaceName ? `
|
|
2702
|
+
interfaceName: ${safeStr(block.interfaceName)},` : "";
|
|
2703
|
+
const exportName = block.interfaceName ?? toCamelCase(block.slug);
|
|
2704
|
+
return [
|
|
2705
|
+
imports.join("\n"),
|
|
2706
|
+
"",
|
|
2707
|
+
`export const ${exportName}: Block = {`,
|
|
2708
|
+
` slug: ${safeStr(block.slug)},${interfaceLine}${labelsCode}`,
|
|
2709
|
+
` fields: [`,
|
|
2710
|
+
fieldsCode,
|
|
2711
|
+
` ],`,
|
|
2712
|
+
`}`,
|
|
2713
|
+
""
|
|
2714
|
+
].join("\n");
|
|
2715
|
+
}
|
|
2716
|
+
function containsRichText(fields) {
|
|
2717
|
+
return fields.some((f) => {
|
|
2718
|
+
if (f.type === "richtext") return true;
|
|
2719
|
+
if (f.fields && containsRichText(f.fields)) return true;
|
|
2720
|
+
if (f.tabs?.some((tab) => containsRichText(tab.fields ?? []))) return true;
|
|
2721
|
+
return false;
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
function toCamelCase(slug) {
|
|
2725
|
+
return slug.split(/[-_]/).map(
|
|
2726
|
+
(part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
|
|
2727
|
+
).join("");
|
|
2728
|
+
}
|
|
2729
|
+
function toPascalCase(slug) {
|
|
2730
|
+
return slug.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
2731
|
+
}
|
|
2732
|
+
function getTsType(field) {
|
|
2733
|
+
switch (field.type) {
|
|
2734
|
+
case "number":
|
|
2735
|
+
return "number";
|
|
2736
|
+
case "checkbox":
|
|
2737
|
+
return "boolean";
|
|
2738
|
+
case "text":
|
|
2739
|
+
case "textarea":
|
|
2740
|
+
case "email":
|
|
2741
|
+
case "url":
|
|
2742
|
+
case "color":
|
|
2743
|
+
case "select":
|
|
2744
|
+
case "date":
|
|
2745
|
+
return "string";
|
|
2746
|
+
case "multiselect":
|
|
2747
|
+
return "string[]";
|
|
2748
|
+
case "group":
|
|
2749
|
+
return field.fields ? objectType(flattenProps(field.fields)) : "any";
|
|
2750
|
+
case "array":
|
|
2751
|
+
if (field.fields) {
|
|
2752
|
+
const inner = flattenProps(field.fields).map((p) => `${p.name}: ${p.type}`).join("; ");
|
|
2753
|
+
return `Array<{ id: string; ${inner} }>`;
|
|
2754
|
+
}
|
|
2755
|
+
return "any[]";
|
|
2756
|
+
default:
|
|
2757
|
+
return "any";
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
function objectType(props) {
|
|
2761
|
+
return props.length > 0 ? `{ ${props.map((p) => `${p.name}: ${p.type}`).join("; ")} }` : "Record<string, unknown>";
|
|
2762
|
+
}
|
|
2763
|
+
function flattenProps(fields) {
|
|
2764
|
+
const out = [];
|
|
2765
|
+
for (const f of fields) {
|
|
2766
|
+
if (f.type === "row" || f.type === "collapsible") {
|
|
2767
|
+
out.push(...flattenProps(f.fields ?? []));
|
|
2768
|
+
} else if (f.type === "tabs") {
|
|
2769
|
+
for (const tab of f.tabs ?? []) {
|
|
2770
|
+
if (tab.name) {
|
|
2771
|
+
out.push({ name: tab.name, type: objectType(flattenProps(tab.fields ?? [])) });
|
|
2772
|
+
} else {
|
|
2773
|
+
out.push(...flattenProps(tab.fields ?? []));
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
} else {
|
|
2777
|
+
out.push({ name: f.name, type: getTsType(f) });
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
return out;
|
|
2781
|
+
}
|
|
2782
|
+
function generateReactComponent(block) {
|
|
2783
|
+
const componentName = block.interfaceName ?? toPascalCase(block.slug);
|
|
2784
|
+
const propsName = `${componentName}Props`;
|
|
2785
|
+
const propList = flattenProps(block.fields);
|
|
2786
|
+
const propsCode = propList.map((p) => ` ${p.name}: ${p.type}`).join("\n");
|
|
2787
|
+
const fieldsJsx = propList.map((p) => ` <div className="field-${p.name}">
|
|
2788
|
+
{/* ${p.name} */}
|
|
2789
|
+
{String(props.${p.name})}
|
|
2790
|
+
</div>`).join("\n");
|
|
2791
|
+
const code = [
|
|
2792
|
+
`import React from 'react'`,
|
|
2793
|
+
``,
|
|
2794
|
+
`export type ${propsName} = {`,
|
|
2795
|
+
propsCode,
|
|
2796
|
+
`}`,
|
|
2797
|
+
``,
|
|
2798
|
+
`export function ${componentName}(props: ${propsName}) {`,
|
|
2799
|
+
` return (`,
|
|
2800
|
+
` <div className="${block.slug}">`,
|
|
2801
|
+
fieldsJsx,
|
|
2802
|
+
` </div>`,
|
|
2803
|
+
` )`,
|
|
2804
|
+
`}`,
|
|
2805
|
+
``
|
|
2806
|
+
].join("\n");
|
|
2807
|
+
return {
|
|
2808
|
+
filename: `${componentName}.tsx`,
|
|
2809
|
+
code,
|
|
2810
|
+
language: "typescript"
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
function generateBlockOutput(block) {
|
|
2814
|
+
return {
|
|
2815
|
+
filename: `${block.slug}.ts`,
|
|
2816
|
+
code: generateBlockCode(block),
|
|
2817
|
+
language: "typescript"
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
function generateAllBlocks(blocks, options = {}) {
|
|
2821
|
+
return blocks.flatMap(
|
|
2822
|
+
(block) => options.react ? [generateBlockOutput(block), generateReactComponent(block)] : [generateBlockOutput(block)]
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
function generateIndexFile(blocks) {
|
|
2826
|
+
const exportName = (b) => b.interfaceName ?? toCamelCase(b.slug);
|
|
2827
|
+
const imports = blocks.map((b) => `import { ${exportName(b)} } from './${b.slug}'`).join("\n");
|
|
2828
|
+
const exportList = blocks.map((b) => ` ${exportName(b)}`).join(",\n");
|
|
2829
|
+
const code = [
|
|
2830
|
+
imports,
|
|
2831
|
+
"",
|
|
2832
|
+
`export const blocks = [`,
|
|
2833
|
+
exportList,
|
|
2834
|
+
`] as const`,
|
|
2835
|
+
""
|
|
2836
|
+
].join("\n");
|
|
2837
|
+
return { filename: "index.ts", code, language: "typescript" };
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// src/block-builder/components/canvas/CodePreview.tsx
|
|
2223
2841
|
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
2224
2842
|
"import",
|
|
2225
2843
|
"export",
|
|
@@ -2307,7 +2925,7 @@ var TOKEN_COLORS = {
|
|
|
2307
2925
|
};
|
|
2308
2926
|
function HighlightedCode({ code }) {
|
|
2309
2927
|
const tokens = tokenise(code);
|
|
2310
|
-
return /* @__PURE__ */
|
|
2928
|
+
return /* @__PURE__ */ React16.createElement("code", { style: { fontFamily: "inherit" } }, tokens.map((tok, i) => /* @__PURE__ */ React16.createElement("span", { key: i, style: { color: TOKEN_COLORS[tok.kind] } }, tok.text)));
|
|
2311
2929
|
}
|
|
2312
2930
|
function CodePreview() {
|
|
2313
2931
|
const blocks = useBuilderStore((s) => s.blocks);
|
|
@@ -2322,7 +2940,7 @@ function CodePreview() {
|
|
|
2322
2940
|
setActiveFile(null);
|
|
2323
2941
|
return;
|
|
2324
2942
|
}
|
|
2325
|
-
const blockOutputs = generateAllBlocks(blocks);
|
|
2943
|
+
const blockOutputs = generateAllBlocks(blocks, { react: true });
|
|
2326
2944
|
const indexOutput = generateIndexFile(blocks);
|
|
2327
2945
|
const next = {};
|
|
2328
2946
|
for (const out of blockOutputs) next[out.filename] = out.code;
|
|
@@ -2352,7 +2970,7 @@ function CodePreview() {
|
|
|
2352
2970
|
const activeCode = activeFile ? fileMap[activeFile] ?? "" : "";
|
|
2353
2971
|
const lineCount = activeCode ? activeCode.split("\n").length : 0;
|
|
2354
2972
|
if (fileNames.length === 0) {
|
|
2355
|
-
return /* @__PURE__ */
|
|
2973
|
+
return /* @__PURE__ */ React16.createElement(
|
|
2356
2974
|
"div",
|
|
2357
2975
|
{
|
|
2358
2976
|
className: "code-preview",
|
|
@@ -2369,7 +2987,7 @@ function CodePreview() {
|
|
|
2369
2987
|
"Add a block to see generated TypeScript code"
|
|
2370
2988
|
);
|
|
2371
2989
|
}
|
|
2372
|
-
return /* @__PURE__ */
|
|
2990
|
+
return /* @__PURE__ */ React16.createElement("div", { className: "code-preview", style: { height: "100%", display: "flex", flexDirection: "column", overflow: "hidden" } }, /* @__PURE__ */ React16.createElement(
|
|
2373
2991
|
"div",
|
|
2374
2992
|
{
|
|
2375
2993
|
style: {
|
|
@@ -2383,7 +3001,7 @@ function CodePreview() {
|
|
|
2383
3001
|
},
|
|
2384
3002
|
fileNames.map((name) => {
|
|
2385
3003
|
const isActive = activeFile === name;
|
|
2386
|
-
return /* @__PURE__ */
|
|
3004
|
+
return /* @__PURE__ */ React16.createElement(
|
|
2387
3005
|
"button",
|
|
2388
3006
|
{
|
|
2389
3007
|
key: name,
|
|
@@ -2406,8 +3024,8 @@ function CodePreview() {
|
|
|
2406
3024
|
name
|
|
2407
3025
|
);
|
|
2408
3026
|
}),
|
|
2409
|
-
/* @__PURE__ */
|
|
2410
|
-
lineCount > 0 && /* @__PURE__ */
|
|
3027
|
+
/* @__PURE__ */ React16.createElement("div", { style: { flex: 1 } }),
|
|
3028
|
+
lineCount > 0 && /* @__PURE__ */ React16.createElement(
|
|
2411
3029
|
"span",
|
|
2412
3030
|
{
|
|
2413
3031
|
style: {
|
|
@@ -2420,7 +3038,7 @@ function CodePreview() {
|
|
|
2420
3038
|
lineCount,
|
|
2421
3039
|
" lines"
|
|
2422
3040
|
),
|
|
2423
|
-
/* @__PURE__ */
|
|
3041
|
+
/* @__PURE__ */ React16.createElement(
|
|
2424
3042
|
"button",
|
|
2425
3043
|
{
|
|
2426
3044
|
type: "button",
|
|
@@ -2443,7 +3061,7 @@ function CodePreview() {
|
|
|
2443
3061
|
},
|
|
2444
3062
|
copied ? "Copied" : "Copy"
|
|
2445
3063
|
)
|
|
2446
|
-
), /* @__PURE__ */
|
|
3064
|
+
), /* @__PURE__ */ React16.createElement("div", { style: { flex: 1, overflow: "auto", display: "flex" } }, /* @__PURE__ */ React16.createElement(
|
|
2447
3065
|
"div",
|
|
2448
3066
|
{
|
|
2449
3067
|
style: {
|
|
@@ -2460,8 +3078,8 @@ function CodePreview() {
|
|
|
2460
3078
|
},
|
|
2461
3079
|
"aria-hidden": "true"
|
|
2462
3080
|
},
|
|
2463
|
-
activeCode.split("\n").map((_, i) => /* @__PURE__ */
|
|
2464
|
-
), /* @__PURE__ */
|
|
3081
|
+
activeCode.split("\n").map((_, i) => /* @__PURE__ */ React16.createElement("div", { key: i }, i + 1))
|
|
3082
|
+
), /* @__PURE__ */ React16.createElement(
|
|
2465
3083
|
"pre",
|
|
2466
3084
|
{
|
|
2467
3085
|
style: {
|
|
@@ -2476,37 +3094,198 @@ function CodePreview() {
|
|
|
2476
3094
|
whiteSpace: "pre"
|
|
2477
3095
|
}
|
|
2478
3096
|
},
|
|
2479
|
-
/* @__PURE__ */
|
|
3097
|
+
/* @__PURE__ */ React16.createElement(HighlightedCode, { code: activeCode })
|
|
2480
3098
|
)));
|
|
2481
3099
|
}
|
|
2482
3100
|
|
|
3101
|
+
// src/block-builder/components/canvas/LivePreview.tsx
|
|
3102
|
+
import React17, { useState as useState8 } from "react";
|
|
3103
|
+
import {
|
|
3104
|
+
Type as Type3,
|
|
3105
|
+
AlignLeft as AlignLeft3,
|
|
3106
|
+
AlignJustify as AlignJustify3,
|
|
3107
|
+
Hash as Hash3,
|
|
3108
|
+
Mail as Mail3,
|
|
3109
|
+
Calendar as Calendar3,
|
|
3110
|
+
CheckSquare as CheckSquare3,
|
|
3111
|
+
ChevronDown as ChevronDown4,
|
|
3112
|
+
Upload as Upload3,
|
|
3113
|
+
Image as ImageIcon,
|
|
3114
|
+
Link as LinkIcon,
|
|
3115
|
+
Braces as Braces3,
|
|
3116
|
+
List as List3,
|
|
3117
|
+
Box as Box3,
|
|
3118
|
+
Columns as Columns3,
|
|
3119
|
+
Folder as Folder3,
|
|
3120
|
+
LayoutTemplate
|
|
3121
|
+
} from "lucide-react";
|
|
3122
|
+
var TYPE_ICON = {
|
|
3123
|
+
text: Type3,
|
|
3124
|
+
textarea: AlignLeft3,
|
|
3125
|
+
richtext: AlignJustify3,
|
|
3126
|
+
number: Hash3,
|
|
3127
|
+
email: Mail3,
|
|
3128
|
+
url: LinkIcon,
|
|
3129
|
+
date: Calendar3,
|
|
3130
|
+
checkbox: CheckSquare3,
|
|
3131
|
+
select: ChevronDown4,
|
|
3132
|
+
image: ImageIcon,
|
|
3133
|
+
file: Upload3,
|
|
3134
|
+
relationship: LinkIcon,
|
|
3135
|
+
json: Braces3,
|
|
3136
|
+
array: List3,
|
|
3137
|
+
group: Box3,
|
|
3138
|
+
row: Columns3,
|
|
3139
|
+
tabs: Folder3,
|
|
3140
|
+
collapsible: ChevronDown4
|
|
3141
|
+
};
|
|
3142
|
+
function TypeIcon({ type }) {
|
|
3143
|
+
const Icon = TYPE_ICON[type] ?? Type3;
|
|
3144
|
+
return /* @__PURE__ */ React17.createElement(Icon, { size: 13, strokeWidth: 1.75 });
|
|
3145
|
+
}
|
|
3146
|
+
function fieldDisplayLabel(field) {
|
|
3147
|
+
return field.label || field.name || "Untitled field";
|
|
3148
|
+
}
|
|
3149
|
+
function fullFieldPath(field, prefix) {
|
|
3150
|
+
const name = field.name || "\u2014";
|
|
3151
|
+
return prefix ? `${prefix}.${name}` : name;
|
|
3152
|
+
}
|
|
3153
|
+
function fieldPlaceholder(field) {
|
|
3154
|
+
if (field.admin?.placeholder) return field.admin.placeholder;
|
|
3155
|
+
switch (field.type) {
|
|
3156
|
+
case "email":
|
|
3157
|
+
return "name@example.com";
|
|
3158
|
+
case "url":
|
|
3159
|
+
return "https://example.com";
|
|
3160
|
+
case "textarea":
|
|
3161
|
+
case "richtext":
|
|
3162
|
+
return `Write ${fieldDisplayLabel(field).toLowerCase()} here\u2026`;
|
|
3163
|
+
case "text":
|
|
3164
|
+
default:
|
|
3165
|
+
return `Enter ${fieldDisplayLabel(field).toLowerCase()}`;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
function pluralize(count, word) {
|
|
3169
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
3170
|
+
}
|
|
3171
|
+
function FieldMeta({ field, path }) {
|
|
3172
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field__meta" }, path !== null && /* @__PURE__ */ React17.createElement("code", { className: "bb-preview-field__slug" }, path), /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-field__badge" }, field.type));
|
|
3173
|
+
}
|
|
3174
|
+
function FieldHead({ field, prefix }) {
|
|
3175
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field__head" }, /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-field__icon" }, /* @__PURE__ */ React17.createElement(TypeIcon, { type: field.type })), /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field__headtext" }, /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-field__label" }, fieldDisplayLabel(field), field.required && /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-field__required" }, "*")), /* @__PURE__ */ React17.createElement(FieldMeta, { field, path: fullFieldPath(field, prefix) })));
|
|
3176
|
+
}
|
|
3177
|
+
function PreviewField({ field, prefix = "" }) {
|
|
3178
|
+
switch (field.type) {
|
|
3179
|
+
case "text":
|
|
3180
|
+
case "url":
|
|
3181
|
+
case "email":
|
|
3182
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ React17.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ React17.createElement("input", { className: "bb-preview-input", disabled: true, placeholder: fieldPlaceholder(field) }));
|
|
3183
|
+
case "textarea":
|
|
3184
|
+
case "richtext":
|
|
3185
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ React17.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ React17.createElement("textarea", { className: "bb-preview-textarea", disabled: true, rows: 3, placeholder: fieldPlaceholder(field) }));
|
|
3186
|
+
case "number":
|
|
3187
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ React17.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ React17.createElement("input", { className: "bb-preview-input", type: "number", disabled: true, placeholder: field.admin?.placeholder ?? "0" }));
|
|
3188
|
+
case "date":
|
|
3189
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ React17.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ React17.createElement("input", { className: "bb-preview-input", type: "date", disabled: true }));
|
|
3190
|
+
case "checkbox":
|
|
3191
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ React17.createElement("label", { className: "bb-preview-checkbox" }, /* @__PURE__ */ React17.createElement("input", { type: "checkbox", className: "bb-preview-checkbox__input", disabled: true }), /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-field__icon" }, /* @__PURE__ */ React17.createElement(TypeIcon, { type: field.type })), /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field__headtext" }, /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-field__label" }, fieldDisplayLabel(field), field.required && /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-field__required" }, "*")), /* @__PURE__ */ React17.createElement(FieldMeta, { field, path: fullFieldPath(field, prefix) }))));
|
|
3192
|
+
case "select":
|
|
3193
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ React17.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ React17.createElement("select", { className: "bb-preview-select", disabled: true }, /* @__PURE__ */ React17.createElement("option", null, field.options?.[0]?.label ?? "Select an option")));
|
|
3194
|
+
case "json":
|
|
3195
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ React17.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-placeholder" }, "Raw JSON data"));
|
|
3196
|
+
case "image":
|
|
3197
|
+
case "file":
|
|
3198
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ React17.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-placeholder bb-preview-placeholder--upload" }, /* @__PURE__ */ React17.createElement(TypeIcon, { type: field.type }), field.type === "image" ? "Image upload" : "File upload"));
|
|
3199
|
+
case "relationship":
|
|
3200
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ React17.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-placeholder bb-preview-placeholder--upload" }, /* @__PURE__ */ React17.createElement(TypeIcon, { type: field.type }), field.collection ? `Linked to \u201C${field.collection}\u201D` : "No collection set", field.hasMany ? " \xB7 multiple" : ""));
|
|
3201
|
+
case "array": {
|
|
3202
|
+
const ownPath = fullFieldPath(field, prefix);
|
|
3203
|
+
return /* @__PURE__ */ React17.createElement(PreviewGroup, { field, ownPath, sublabel: `repeating \xB7 ${pluralize(field.fields?.length ?? 0, "field")} per row` }, (field.fields ?? []).map((f) => /* @__PURE__ */ React17.createElement(PreviewField, { key: f.id, field: f, prefix: `${ownPath}[]` })));
|
|
3204
|
+
}
|
|
3205
|
+
case "group": {
|
|
3206
|
+
const ownPath = fullFieldPath(field, prefix);
|
|
3207
|
+
return /* @__PURE__ */ React17.createElement(PreviewGroup, { field, ownPath }, (field.fields ?? []).map((f) => /* @__PURE__ */ React17.createElement(PreviewField, { key: f.id, field: f, prefix: ownPath })));
|
|
3208
|
+
}
|
|
3209
|
+
case "collapsible":
|
|
3210
|
+
return /* @__PURE__ */ React17.createElement(PreviewGroup, { field, ownPath: null }, (field.fields ?? []).map((f) => /* @__PURE__ */ React17.createElement(PreviewField, { key: f.id, field: f, prefix })));
|
|
3211
|
+
case "row":
|
|
3212
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-row" }, (field.fields ?? []).map((f) => /* @__PURE__ */ React17.createElement(PreviewField, { key: f.id, field: f, prefix })));
|
|
3213
|
+
case "tabs":
|
|
3214
|
+
return /* @__PURE__ */ React17.createElement(PreviewTabs, { field, prefix });
|
|
3215
|
+
default:
|
|
3216
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field" }, /* @__PURE__ */ React17.createElement(FieldHead, { field, prefix }), /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-placeholder" }, "Unsupported preview for \u201C", field.type, "\u201D"));
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
function PreviewGroup({ field, ownPath, sublabel, children }) {
|
|
3220
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-group" }, /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-group__header" }, /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-field__icon" }, /* @__PURE__ */ React17.createElement(TypeIcon, { type: field.type })), /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field__headtext" }, /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-field__label" }, fieldDisplayLabel(field), sublabel && /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-group__sublabel" }, sublabel)), /* @__PURE__ */ React17.createElement(FieldMeta, { field, path: ownPath }))), /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-group__body" }, React17.Children.count(children) > 0 ? children : /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-placeholder" }, "No fields inside yet")));
|
|
3221
|
+
}
|
|
3222
|
+
function PreviewTabs({ field, prefix }) {
|
|
3223
|
+
const [active, setActive] = useState8(0);
|
|
3224
|
+
const tabs = field.tabs ?? [];
|
|
3225
|
+
const tab = tabs[active];
|
|
3226
|
+
const tabPrefix = tab?.name ? prefix ? `${prefix}.${tab.name}` : tab.name : prefix;
|
|
3227
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-group" }, /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-group__header" }, /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-field__icon" }, /* @__PURE__ */ React17.createElement(TypeIcon, { type: field.type })), /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-field__headtext" }, /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-field__label" }, fieldDisplayLabel(field)), /* @__PURE__ */ React17.createElement(FieldMeta, { field, path: null }))), /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-tabs__list" }, tabs.length === 0 && /* @__PURE__ */ React17.createElement("span", { className: "bb-preview-placeholder", style: { margin: 8 } }, "No tabs yet"), tabs.map((t, i) => /* @__PURE__ */ React17.createElement(
|
|
3228
|
+
"button",
|
|
3229
|
+
{
|
|
3230
|
+
key: t.id ?? i,
|
|
3231
|
+
type: "button",
|
|
3232
|
+
className: `bb-preview-tabs__tab${i === active ? " bb-preview-tabs__tab--active" : ""}`,
|
|
3233
|
+
onClick: () => setActive(i)
|
|
3234
|
+
},
|
|
3235
|
+
t.label
|
|
3236
|
+
))), tab && /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-group__body" }, tab.name && /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-tabs__path" }, "Nested under ", /* @__PURE__ */ React17.createElement("code", { className: "bb-preview-field__slug" }, tabPrefix)), (tab.fields ?? []).length > 0 ? (tab.fields ?? []).map((f) => /* @__PURE__ */ React17.createElement(PreviewField, { key: f.id, field: f, prefix: tabPrefix })) : /* @__PURE__ */ React17.createElement("div", { className: "bb-preview-placeholder" }, "No fields inside yet")));
|
|
3237
|
+
}
|
|
3238
|
+
function LivePreview() {
|
|
3239
|
+
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
3240
|
+
const blocks = useBuilderStore((s) => s.blocks);
|
|
3241
|
+
const block = blocks.find((b) => b.id === activeBlockId);
|
|
3242
|
+
if (!block || block.fields.length === 0) {
|
|
3243
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-live-preview bb-live-preview--empty" }, /* @__PURE__ */ React17.createElement(LayoutTemplate, { size: 32, strokeWidth: 1.5, color: "var(--bb-text-subtle)", style: { marginBottom: 16 } }), /* @__PURE__ */ React17.createElement("p", { style: { fontWeight: 600, marginBottom: 8, color: "var(--bb-text)" } }, "Nothing to preview yet"), /* @__PURE__ */ React17.createElement("p", { style: { fontSize: 12, color: "var(--bb-text-subtle)", textAlign: "center", maxWidth: 260 } }, "Add fields from the palette on the left \u2014 this panel updates live as you build."));
|
|
3244
|
+
}
|
|
3245
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "bb-live-preview" }, /* @__PURE__ */ React17.createElement("div", { className: "bb-live-preview__header" }, /* @__PURE__ */ React17.createElement("span", { className: "bb-live-preview__title" }, "Live Preview"), /* @__PURE__ */ React17.createElement("span", { className: "bb-live-preview__meta" }, block.labels?.singular ?? block.slug, " \xB7 ", pluralize(block.fields.length, "field"))), /* @__PURE__ */ React17.createElement("div", { className: "bb-live-preview__body" }, /* @__PURE__ */ React17.createElement("div", { className: "bb-live-preview__page" }, block.fields.map((field) => /* @__PURE__ */ React17.createElement(PreviewField, { key: field.id, field })))));
|
|
3246
|
+
}
|
|
3247
|
+
|
|
2483
3248
|
// src/block-builder/components/canvas/BuilderShell.tsx
|
|
2484
3249
|
function BuilderShell({ loadSlug }) {
|
|
2485
3250
|
const loadBlock = useBuilderStore((s) => s.loadBlock);
|
|
2486
3251
|
const setVersionMeta = useBuilderStore((s) => s.setVersionMeta);
|
|
2487
3252
|
const setBlockSlug = useBuilderStore((s) => s.setBlockSlug);
|
|
2488
3253
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
2489
|
-
const [activeSlug, setActiveSlug] =
|
|
2490
|
-
const [loading, setLoading] =
|
|
2491
|
-
const [notification, setNotification] =
|
|
2492
|
-
const [showCodePreview, setShowCodePreview] =
|
|
2493
|
-
const [versions, setVersions] =
|
|
2494
|
-
const [selectedVersionId, setSelectedVersionId] =
|
|
2495
|
-
const [blockDefs, setBlockDefs] =
|
|
2496
|
-
const [mobilePanelTab, setMobilePanelTab] =
|
|
3254
|
+
const [activeSlug, setActiveSlug] = useState9(loadSlug ?? null);
|
|
3255
|
+
const [loading, setLoading] = useState9(!!loadSlug);
|
|
3256
|
+
const [notification, setNotification] = useState9(null);
|
|
3257
|
+
const [showCodePreview, setShowCodePreview] = useState9(false);
|
|
3258
|
+
const [versions, setVersions] = useState9([]);
|
|
3259
|
+
const [selectedVersionId, setSelectedVersionId] = useState9(null);
|
|
3260
|
+
const [blockDefs, setBlockDefs] = useState9([]);
|
|
3261
|
+
const [mobilePanelTab, setMobilePanelTab] = useState9("blocks");
|
|
3262
|
+
const [isMounted, setIsMounted] = useState9(false);
|
|
3263
|
+
const [previewOpen, setPreviewOpen] = useState9(false);
|
|
2497
3264
|
useEffect6(() => {
|
|
2498
|
-
|
|
3265
|
+
setIsMounted(true);
|
|
3266
|
+
}, []);
|
|
3267
|
+
const refreshBlockDefs = useCallback5(async (reportErrors = false) => {
|
|
3268
|
+
try {
|
|
3269
|
+
const res = await fetch("/api/block-definitions?limit=200&depth=0");
|
|
3270
|
+
const json = await res.json();
|
|
2499
3271
|
setBlockDefs(
|
|
2500
3272
|
(json.docs ?? []).map((d) => ({ id: String(d.id), slug: d.slug, name: d.name }))
|
|
2501
3273
|
);
|
|
2502
|
-
}
|
|
3274
|
+
} catch (err) {
|
|
2503
3275
|
console.error("[block-builder] Failed to load block definitions:", err);
|
|
2504
|
-
|
|
2505
|
-
|
|
3276
|
+
if (reportErrors) {
|
|
3277
|
+
setNotification({ status: "error", title: "Failed to load block definitions", errors: ["Could not load block definitions. Please refresh the page."] });
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
2506
3280
|
}, []);
|
|
3281
|
+
useEffect6(() => {
|
|
3282
|
+
refreshBlockDefs(true);
|
|
3283
|
+
}, [refreshBlockDefs]);
|
|
2507
3284
|
const loadVersionsForSlug = useCallback5(async (slug) => {
|
|
2508
3285
|
try {
|
|
2509
|
-
const res = await fetch(`/api/block-builder/versions/${encodeURIComponent(slug)}
|
|
3286
|
+
const res = await fetch(`/api/block-builder/versions/${encodeURIComponent(slug)}`, {
|
|
3287
|
+
headers: { "X-Block-Builder": "1" }
|
|
3288
|
+
});
|
|
2510
3289
|
const json = await res.json();
|
|
2511
3290
|
return json.versions ?? [];
|
|
2512
3291
|
} catch {
|
|
@@ -2518,7 +3297,9 @@ function BuilderShell({ loadSlug }) {
|
|
|
2518
3297
|
setNotification(null);
|
|
2519
3298
|
const url = versionId ? `/api/block-builder/load/${encodeURIComponent(slug)}?versionId=${encodeURIComponent(versionId)}` : `/api/block-builder/load/${encodeURIComponent(slug)}`;
|
|
2520
3299
|
try {
|
|
2521
|
-
const res = await fetch(url
|
|
3300
|
+
const res = await fetch(url, {
|
|
3301
|
+
headers: { "X-Block-Builder": "1" }
|
|
3302
|
+
});
|
|
2522
3303
|
const json = await res.json();
|
|
2523
3304
|
if (json.block) {
|
|
2524
3305
|
loadBlock(json.block);
|
|
@@ -2562,14 +3343,22 @@ function BuilderShell({ loadSlug }) {
|
|
|
2562
3343
|
const current = list.find((v) => v.isCurrent) ?? list[0];
|
|
2563
3344
|
if (current) setSelectedVersionId(current.id);
|
|
2564
3345
|
}
|
|
2565
|
-
async function handleAfterPublish() {
|
|
2566
|
-
|
|
2567
|
-
|
|
3346
|
+
async function handleAfterPublish(publishedSlug) {
|
|
3347
|
+
setActiveSlug(publishedSlug);
|
|
3348
|
+
setBlockSlug(publishedSlug);
|
|
3349
|
+
const [list] = await Promise.all([
|
|
3350
|
+
loadVersionsForSlug(publishedSlug),
|
|
3351
|
+
// A first publish creates a definition the picker has never seen.
|
|
3352
|
+
refreshBlockDefs()
|
|
3353
|
+
]);
|
|
2568
3354
|
setVersions(list);
|
|
2569
3355
|
const current = list.find((v) => v.isCurrent) ?? list[0];
|
|
2570
|
-
|
|
3356
|
+
setSelectedVersionId(current ? current.id : null);
|
|
3357
|
+
}
|
|
3358
|
+
if (!isMounted) {
|
|
3359
|
+
return /* @__PURE__ */ React18.createElement("div", { className: "bb-shell" }, /* @__PURE__ */ React18.createElement("div", { className: "bb-loading-bar" }, "Initializing Builder..."));
|
|
2571
3360
|
}
|
|
2572
|
-
return /* @__PURE__ */
|
|
3361
|
+
return /* @__PURE__ */ React18.createElement(ErrorBoundary, null, /* @__PURE__ */ React18.createElement("div", { className: "bb-shell" }, /* @__PURE__ */ React18.createElement(
|
|
2573
3362
|
TopBar,
|
|
2574
3363
|
{
|
|
2575
3364
|
blockDefs,
|
|
@@ -2581,9 +3370,11 @@ function BuilderShell({ loadSlug }) {
|
|
|
2581
3370
|
onRestoreVersion: handleRestoreVersion,
|
|
2582
3371
|
onAfterPublish: handleAfterPublish,
|
|
2583
3372
|
notification,
|
|
2584
|
-
onSetNotification: setNotification
|
|
3373
|
+
onSetNotification: setNotification,
|
|
3374
|
+
previewOpen,
|
|
3375
|
+
onTogglePreview: () => setPreviewOpen((p) => !p)
|
|
2585
3376
|
}
|
|
2586
|
-
), loading && /* @__PURE__ */
|
|
3377
|
+
), loading && /* @__PURE__ */ React18.createElement("div", { className: "bb-loading-bar" }, "Loading..."), isReadOnly && !loading && /* @__PURE__ */ React18.createElement("div", { className: "bb-readonly-banner" }, /* @__PURE__ */ React18.createElement("span", { className: "bb-readonly-banner__icon" }, "[i]"), /* @__PURE__ */ React18.createElement("span", null, "You are viewing a previous version - read only.", /* @__PURE__ */ React18.createElement(
|
|
2587
3378
|
"button",
|
|
2588
3379
|
{
|
|
2589
3380
|
type: "button",
|
|
@@ -2591,7 +3382,7 @@ function BuilderShell({ loadSlug }) {
|
|
|
2591
3382
|
onClick: handleRestoreVersion
|
|
2592
3383
|
},
|
|
2593
3384
|
"Switch to latest"
|
|
2594
|
-
))), /* @__PURE__ */
|
|
3385
|
+
))), /* @__PURE__ */ React18.createElement("div", { className: "bb-main", "data-mobile-panel": mobilePanelTab }, /* @__PURE__ */ React18.createElement(BlockList, { blockDefs, activeSlug, onBlockSelect: loadBlockBySlug }), /* @__PURE__ */ React18.createElement("div", { className: "bb-main__center", style: { display: "flex", flex: 1 } }, /* @__PURE__ */ React18.createElement(FieldPalette, null), /* @__PURE__ */ React18.createElement(BuilderCanvas, null), previewOpen && /* @__PURE__ */ React18.createElement(LivePreview, null)), /* @__PURE__ */ React18.createElement(ConfigPanel, null)), /* @__PURE__ */ React18.createElement("div", { className: "bb-footer" }, /* @__PURE__ */ React18.createElement(
|
|
2595
3386
|
"button",
|
|
2596
3387
|
{
|
|
2597
3388
|
type: "button",
|
|
@@ -2600,12 +3391,12 @@ function BuilderShell({ loadSlug }) {
|
|
|
2600
3391
|
},
|
|
2601
3392
|
showCodePreview ? "v" : ">",
|
|
2602
3393
|
" Code Preview"
|
|
2603
|
-
), showCodePreview && /* @__PURE__ */
|
|
3394
|
+
), showCodePreview && /* @__PURE__ */ React18.createElement("div", { className: "bb-footer__content" }, /* @__PURE__ */ React18.createElement(CodePreview, null))), /* @__PURE__ */ React18.createElement("nav", { className: "bb-mobile-nav", "aria-label": "Panel navigation" }, [
|
|
2604
3395
|
{ id: "blocks", icon: "B", label: "Blocks" },
|
|
2605
3396
|
{ id: "canvas", icon: "[]", label: "Canvas" },
|
|
2606
3397
|
{ id: "palette", icon: "+", label: "Fields" },
|
|
2607
3398
|
{ id: "config", icon: "*", label: "Config" }
|
|
2608
|
-
].map(({ id, icon, label }) => /* @__PURE__ */
|
|
3399
|
+
].map(({ id, icon, label }) => /* @__PURE__ */ React18.createElement(
|
|
2609
3400
|
"button",
|
|
2610
3401
|
{
|
|
2611
3402
|
key: id,
|
|
@@ -2613,16 +3404,16 @@ function BuilderShell({ loadSlug }) {
|
|
|
2613
3404
|
className: `bb-mobile-nav__tab${mobilePanelTab === id ? " bb-mobile-nav__tab--active" : ""}`,
|
|
2614
3405
|
onClick: () => setMobilePanelTab(id)
|
|
2615
3406
|
},
|
|
2616
|
-
/* @__PURE__ */
|
|
3407
|
+
/* @__PURE__ */ React18.createElement("span", { className: "bb-mobile-nav__icon" }, icon),
|
|
2617
3408
|
label
|
|
2618
|
-
))));
|
|
3409
|
+
)))));
|
|
2619
3410
|
}
|
|
2620
3411
|
|
|
2621
3412
|
// src/components/BlockBuilderNavLink/index.tsx
|
|
2622
|
-
import
|
|
3413
|
+
import React19 from "react";
|
|
2623
3414
|
import Link3 from "next/link";
|
|
2624
3415
|
function BlockBuilderNavLink() {
|
|
2625
|
-
return /* @__PURE__ */
|
|
3416
|
+
return /* @__PURE__ */ React19.createElement("div", { style: { padding: "0 16px", marginTop: "8px" } }, /* @__PURE__ */ React19.createElement(
|
|
2626
3417
|
Link3,
|
|
2627
3418
|
{
|
|
2628
3419
|
href: "/block-builder",
|