@nextbridgehq/payload-block-builder 0.1.8 → 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 +280 -76
- package/dist/bin/init.js +0 -0
- package/dist/client.cjs +1487 -603
- package/dist/client.d.cts +6 -2
- package/dist/client.d.ts +6 -2
- package/dist/client.js +1438 -530
- package/dist/index.cjs +887 -520
- package/dist/index.d.cts +52 -8
- package/dist/index.d.ts +52 -8
- package/dist/index.js +884 -518
- package/package.json +29 -10
- package/src/block-builder/builder.css +391 -36
- package/src/components/BlockDataField/BlockDataField.css +652 -393
- package/src/components/SchemaBuilderField/SchemaBuilderField.css +361 -361
package/dist/client.js
CHANGED
|
@@ -39,72 +39,207 @@ function MediaPicker({ label, required, value, onChange }) {
|
|
|
39
39
|
"x"
|
|
40
40
|
))) : /* @__PURE__ */ React.createElement(ListDrawerToggler, { className: "bdf-upload-btn" }, "Choose from Media Library")), /* @__PURE__ */ React.createElement(ListDrawer, { onSelect: handleSelect }));
|
|
41
41
|
}
|
|
42
|
-
function
|
|
42
|
+
function toRelDoc(v) {
|
|
43
|
+
if (!v) return null;
|
|
44
|
+
if (typeof v === "object") {
|
|
45
|
+
const o = v;
|
|
46
|
+
if (!o.id) return null;
|
|
47
|
+
return { id: o.id, title: o.title ? String(o.title) : null };
|
|
48
|
+
}
|
|
49
|
+
if (typeof v === "string" || typeof v === "number") return { id: v, title: null };
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function RelationshipPicker({ label, required, collection, hasMany = false, value, onChange }) {
|
|
43
53
|
const changeRef = useRef(onChange);
|
|
44
54
|
const closeRef = useRef(() => {
|
|
45
55
|
});
|
|
46
56
|
useEffect(() => {
|
|
47
57
|
changeRef.current = onChange;
|
|
48
58
|
});
|
|
59
|
+
const items = hasMany ? Array.isArray(value) ? value.map(toRelDoc).filter(Boolean) : [] : (() => {
|
|
60
|
+
const d = toRelDoc(value);
|
|
61
|
+
return d ? [d] : [];
|
|
62
|
+
})();
|
|
63
|
+
const itemsRef = useRef(items);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
itemsRef.current = items;
|
|
66
|
+
});
|
|
49
67
|
const handleSelect = useCallback(
|
|
50
68
|
({ docID, doc }) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
const title = String(doc?.title ?? doc?.name ?? doc?.slug ?? "") || null;
|
|
70
|
+
const entry = { id: docID, title };
|
|
71
|
+
if (hasMany) {
|
|
72
|
+
const alreadyExists = itemsRef.current.some((i) => String(i.id) === String(docID));
|
|
73
|
+
if (!alreadyExists) changeRef.current([...itemsRef.current, entry]);
|
|
74
|
+
} else {
|
|
75
|
+
changeRef.current(entry);
|
|
76
|
+
closeRef.current();
|
|
77
|
+
}
|
|
56
78
|
},
|
|
57
|
-
[]
|
|
79
|
+
[hasMany]
|
|
58
80
|
);
|
|
59
|
-
const [ListDrawer, ListDrawerToggler, { closeDrawer }] = useListDrawer({
|
|
81
|
+
const [ListDrawer, ListDrawerToggler, { closeDrawer, openDrawer }] = useListDrawer({
|
|
60
82
|
collectionSlugs: [collection]
|
|
61
83
|
});
|
|
62
84
|
closeRef.current = closeDrawer;
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const relTitle = relObj?.title ? String(relObj.title) : null;
|
|
66
|
-
const [fetchedTitle, setFetchedTitle] = useState(null);
|
|
85
|
+
const [fetchedTitles, setFetchedTitles] = useState({});
|
|
86
|
+
const fetchingRef = useRef(/* @__PURE__ */ new Set());
|
|
67
87
|
useEffect(() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
const missing = items.filter(
|
|
89
|
+
(i) => !i.title && !fetchedTitles[String(i.id)] && !fetchingRef.current.has(String(i.id))
|
|
90
|
+
);
|
|
91
|
+
if (missing.length === 0) return;
|
|
92
|
+
missing.forEach((item) => {
|
|
93
|
+
const idStr = String(item.id);
|
|
94
|
+
fetchingRef.current.add(idStr);
|
|
95
|
+
fetch(`/api/${collection}/${idStr}?depth=0`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : null).then((doc) => {
|
|
96
|
+
if (doc) {
|
|
97
|
+
const t = doc.title ?? doc.name ?? doc.slug ?? null;
|
|
98
|
+
if (t) setFetchedTitles((prev) => ({ ...prev, [idStr]: String(t) }));
|
|
99
|
+
}
|
|
100
|
+
}).catch((err) => {
|
|
101
|
+
console.error("[Block Builder] Failed to load relation title:", err);
|
|
102
|
+
fetchingRef.current.delete(idStr);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}, [items, collection]);
|
|
106
|
+
function getTitle(item) {
|
|
107
|
+
return item.title ?? fetchedTitles[String(item.id)] ?? `ID: ${String(item.id)}`;
|
|
108
|
+
}
|
|
109
|
+
function clearOne(e, id) {
|
|
110
|
+
e.stopPropagation();
|
|
111
|
+
if (hasMany) onChange(items.filter((i) => String(i.id) !== String(id)));
|
|
112
|
+
else onChange(null);
|
|
113
|
+
}
|
|
114
|
+
return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("label", { className: "bdf-label" }, label, required && /* @__PURE__ */ React.createElement("span", { className: "bdf-required" }, "*")), /* @__PURE__ */ React.createElement("div", { className: `bdf-rel${hasMany ? " bdf-rel--multi" : ""}` }, /* @__PURE__ */ React.createElement(
|
|
115
|
+
"div",
|
|
116
|
+
{
|
|
117
|
+
role: "button",
|
|
118
|
+
tabIndex: 0,
|
|
119
|
+
className: "bdf-rel__control",
|
|
120
|
+
onClick: openDrawer,
|
|
121
|
+
onKeyDown: (e) => {
|
|
122
|
+
if (e.key === "Enter" || e.key === " ") openDrawer();
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
/* @__PURE__ */ React.createElement("div", { className: "bdf-rel__values" }, items.length === 0 && /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__placeholder" }, "Select a value..."), !hasMany && items.length > 0 && /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__single" }, getTitle(items[0])), hasMany && items.map((item) => /* @__PURE__ */ React.createElement("span", { key: String(item.id), className: "bdf-rel__chip" }, /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__chip-label" }, getTitle(item)), /* @__PURE__ */ React.createElement(
|
|
126
|
+
"button",
|
|
127
|
+
{
|
|
128
|
+
type: "button",
|
|
129
|
+
className: "bdf-rel__chip-remove",
|
|
130
|
+
onClick: (e) => clearOne(e, item.id),
|
|
131
|
+
"aria-label": `Remove ${getTitle(item)}`
|
|
132
|
+
},
|
|
133
|
+
/* @__PURE__ */ React.createElement("svg", { height: "12", width: "12", viewBox: "0 0 20 20", "aria-hidden": "true", focusable: "false", fill: "currentColor" }, /* @__PURE__ */ React.createElement("path", { d: "M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z" }))
|
|
134
|
+
)))),
|
|
135
|
+
/* @__PURE__ */ React.createElement("div", { className: "bdf-rel__indicators" }, !hasMany && items.length > 0 && /* @__PURE__ */ React.createElement(
|
|
136
|
+
"button",
|
|
137
|
+
{
|
|
138
|
+
type: "button",
|
|
139
|
+
className: "bdf-rel__clear",
|
|
140
|
+
onClick: (e) => clearOne(e, items[0].id),
|
|
141
|
+
"aria-label": "Clear"
|
|
142
|
+
},
|
|
143
|
+
/* @__PURE__ */ React.createElement("svg", { height: "16", width: "16", viewBox: "0 0 20 20", "aria-hidden": "true", focusable: "false", fill: "currentColor" }, /* @__PURE__ */ React.createElement("path", { d: "M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z" }))
|
|
144
|
+
), /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__sep" }), /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__chevron" }, /* @__PURE__ */ React.createElement("svg", { height: "16", width: "16", viewBox: "0 0 20 20", "aria-hidden": "true", focusable: "false", fill: "currentColor" }, /* @__PURE__ */ React.createElement("path", { d: "M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z" }))))
|
|
145
|
+
), hasMany && /* @__PURE__ */ React.createElement(ListDrawerToggler, { className: "bdf-rel__add", onClick: (e) => e.stopPropagation() }, "+")), /* @__PURE__ */ React.createElement("span", { className: "bdf-rel__hint" }, collection, hasMany ? " \xB7 multiple" : ""), /* @__PURE__ */ React.createElement(ListDrawer, { onSelect: handleSelect }));
|
|
146
|
+
}
|
|
147
|
+
function JsonField({ label, required, value, onChange }) {
|
|
148
|
+
const [text, setText] = useState(
|
|
149
|
+
() => value !== void 0 ? JSON.stringify(value, null, 2) : ""
|
|
150
|
+
);
|
|
151
|
+
const [hasError, setHasError] = useState(false);
|
|
152
|
+
return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("label", { className: "bdf-label" }, label, required && /* @__PURE__ */ React.createElement("span", { className: "bdf-required" }, "*"), /* @__PURE__ */ React.createElement("span", { style: { marginLeft: 6, fontSize: 11, color: "var(--theme-elevation-400)", fontWeight: 400 } }, "(JSON)")), /* @__PURE__ */ React.createElement(
|
|
153
|
+
"textarea",
|
|
154
|
+
{
|
|
155
|
+
className: "bdf-input bdf-textarea bdf-mono",
|
|
156
|
+
value: text,
|
|
157
|
+
rows: 4,
|
|
158
|
+
onChange: (e) => {
|
|
159
|
+
setText(e.target.value);
|
|
160
|
+
try {
|
|
161
|
+
onChange(JSON.parse(e.target.value));
|
|
162
|
+
setHasError(false);
|
|
163
|
+
} catch {
|
|
164
|
+
setHasError(true);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
), hasError && /* @__PURE__ */ React.createElement("div", { className: "bdf-error", style: { marginTop: 4 } }, "Invalid JSON \u2014 changes not saved until fixed."));
|
|
169
|
+
}
|
|
170
|
+
function SchemaForm({ schema, value, onChange, depth = 0 }) {
|
|
171
|
+
const set = useCallback(
|
|
172
|
+
(key, val) => onChange({ ...value, [key]: val }),
|
|
173
|
+
[value, onChange]
|
|
174
|
+
);
|
|
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
|
+
}
|
|
182
|
+
if (field.type === "collapsible") {
|
|
183
|
+
return /* @__PURE__ */ React.createElement(CollapsibleSection, { key: field.name, field, value, onChange, depth: depth + 1 });
|
|
71
184
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
76
196
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
82
208
|
"button",
|
|
83
209
|
{
|
|
84
210
|
type: "button",
|
|
85
|
-
className: "bdf-
|
|
86
|
-
|
|
87
|
-
onClick: () => onChange(null)
|
|
211
|
+
className: "bdf-collapsible__header",
|
|
212
|
+
onClick: () => setOpen((o) => !o)
|
|
88
213
|
},
|
|
89
|
-
"\
|
|
90
|
-
|
|
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 })));
|
|
91
217
|
}
|
|
92
|
-
function
|
|
218
|
+
function TabsSection({
|
|
219
|
+
field,
|
|
220
|
+
value,
|
|
221
|
+
onChange,
|
|
222
|
+
depth
|
|
223
|
+
}) {
|
|
224
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
93
225
|
const set = useCallback(
|
|
94
226
|
(key, val) => onChange({ ...value, [key]: val }),
|
|
95
227
|
[value, onChange]
|
|
96
228
|
);
|
|
97
|
-
|
|
98
|
-
|
|
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",
|
|
99
233
|
{
|
|
100
|
-
key:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
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 }))));
|
|
106
241
|
}
|
|
107
|
-
function FieldInput({ field, value, onChange }) {
|
|
242
|
+
function FieldInput({ field, value, onChange, depth }) {
|
|
108
243
|
const label = field.label ?? field.name;
|
|
109
244
|
switch (field.type) {
|
|
110
245
|
case "text":
|
|
@@ -114,7 +249,7 @@ function FieldInput({ field, value, onChange }) {
|
|
|
114
249
|
"input",
|
|
115
250
|
{
|
|
116
251
|
className: "bdf-input",
|
|
117
|
-
type: field.type === "email" ? "email" : "text",
|
|
252
|
+
type: field.type === "email" ? "email" : field.type === "url" ? "url" : "text",
|
|
118
253
|
value: value ?? "",
|
|
119
254
|
onChange: (e) => onChange(e.target.value)
|
|
120
255
|
}
|
|
@@ -167,7 +302,7 @@ function FieldInput({ field, value, onChange }) {
|
|
|
167
302
|
className: "bdf-input",
|
|
168
303
|
type: "number",
|
|
169
304
|
value: numVal,
|
|
170
|
-
onChange: (e) => onChange(e.target.value === "" ?
|
|
305
|
+
onChange: (e) => onChange(e.target.value === "" ? null : e.target.valueAsNumber)
|
|
171
306
|
}
|
|
172
307
|
));
|
|
173
308
|
}
|
|
@@ -227,33 +362,24 @@ function FieldInput({ field, value, onChange }) {
|
|
|
227
362
|
}
|
|
228
363
|
);
|
|
229
364
|
case "relationship": {
|
|
230
|
-
const
|
|
365
|
+
const relField = field;
|
|
366
|
+
if (!relField.collection) {
|
|
367
|
+
return /* @__PURE__ */ React.createElement("div", { className: "bdf-field" }, /* @__PURE__ */ React.createElement("label", { className: "bdf-label" }, label), /* @__PURE__ */ React.createElement("div", { className: "bdf-error" }, "Relationship field ", /* @__PURE__ */ React.createElement("strong", null, field.name), " has no ", /* @__PURE__ */ React.createElement("code", null, "collection"), " defined in its schema."));
|
|
368
|
+
}
|
|
231
369
|
return /* @__PURE__ */ React.createElement(
|
|
232
370
|
RelationshipPicker,
|
|
233
371
|
{
|
|
234
372
|
label,
|
|
235
373
|
required: field.required,
|
|
236
|
-
collection,
|
|
374
|
+
collection: relField.collection,
|
|
375
|
+
hasMany: relField.hasMany ?? false,
|
|
237
376
|
value,
|
|
238
377
|
onChange
|
|
239
378
|
}
|
|
240
379
|
);
|
|
241
380
|
}
|
|
242
381
|
case "json":
|
|
243
|
-
return /* @__PURE__ */ React.createElement(
|
|
244
|
-
"textarea",
|
|
245
|
-
{
|
|
246
|
-
className: "bdf-input bdf-textarea bdf-mono",
|
|
247
|
-
value: value !== void 0 ? JSON.stringify(value, null, 2) : "",
|
|
248
|
-
rows: 4,
|
|
249
|
-
onChange: (e) => {
|
|
250
|
-
try {
|
|
251
|
-
onChange(JSON.parse(e.target.value));
|
|
252
|
-
} catch {
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
));
|
|
382
|
+
return /* @__PURE__ */ React.createElement(JsonField, { label, required: field.required, value, onChange });
|
|
257
383
|
case "array": {
|
|
258
384
|
const rows = Array.isArray(value) ? value : [];
|
|
259
385
|
const subFields = field.fields ?? [];
|
|
@@ -266,7 +392,8 @@ function FieldInput({ field, value, onChange }) {
|
|
|
266
392
|
const next = [...rows];
|
|
267
393
|
next[i] = updated;
|
|
268
394
|
onChange(next);
|
|
269
|
-
}
|
|
395
|
+
},
|
|
396
|
+
depth: depth + 1
|
|
270
397
|
}
|
|
271
398
|
), /* @__PURE__ */ React.createElement(
|
|
272
399
|
"button",
|
|
@@ -290,7 +417,7 @@ function FieldInput({ field, value, onChange }) {
|
|
|
290
417
|
case "group": {
|
|
291
418
|
const groupVal = value ?? {};
|
|
292
419
|
const subFields = field.fields ?? [];
|
|
293
|
-
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 }))));
|
|
294
421
|
}
|
|
295
422
|
default:
|
|
296
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, ")");
|
|
@@ -311,6 +438,10 @@ function BlockDataField({ path }) {
|
|
|
311
438
|
setError(null);
|
|
312
439
|
return;
|
|
313
440
|
}
|
|
441
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(String(versionId))) {
|
|
442
|
+
setError("Invalid version ID format");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
314
445
|
setLoading(true);
|
|
315
446
|
setError(null);
|
|
316
447
|
fetch(`/api/block-definition-versions/${versionId}?depth=0`, { credentials: "same-origin" }).then((r) => {
|
|
@@ -349,9 +480,41 @@ function BlockDataField({ path }) {
|
|
|
349
480
|
)));
|
|
350
481
|
}
|
|
351
482
|
|
|
483
|
+
// src/components/BlockVersionSync/index.tsx
|
|
484
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
485
|
+
import { useField as useField2, useFormFields as useFormFields2 } from "@payloadcms/ui";
|
|
486
|
+
function BlockVersionSync({ path }) {
|
|
487
|
+
const blockVersionPath = path.replace(/\.blockDefinition$/, ".blockVersion");
|
|
488
|
+
const { setValue: setVersion } = useField2({ path: blockVersionPath });
|
|
489
|
+
const blockDefValue = useFormFields2(([fields]) => fields[path]?.value);
|
|
490
|
+
const prevDefIdRef = useRef2(null);
|
|
491
|
+
useEffect2(() => {
|
|
492
|
+
const defId = blockDefValue && typeof blockDefValue === "object" ? blockDefValue.id : typeof blockDefValue === "string" || typeof blockDefValue === "number" ? blockDefValue : null;
|
|
493
|
+
if (defId === prevDefIdRef.current) return;
|
|
494
|
+
prevDefIdRef.current = defId;
|
|
495
|
+
if (!defId) {
|
|
496
|
+
setVersion(null);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(String(defId))) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
fetch(`/api/block-definitions/${String(defId)}?depth=1`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : null).then((doc) => {
|
|
503
|
+
if (!doc) return;
|
|
504
|
+
const currentVersion = doc.currentVersion;
|
|
505
|
+
if (!currentVersion) return;
|
|
506
|
+
const versionId = typeof currentVersion === "object" ? currentVersion.id : currentVersion;
|
|
507
|
+
if (versionId) setVersion(versionId);
|
|
508
|
+
}).catch((err) => {
|
|
509
|
+
console.error("[Block Builder] BlockVersionSync fetch error:", err);
|
|
510
|
+
});
|
|
511
|
+
}, [blockDefValue, setVersion]);
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
352
515
|
// src/components/SchemaBuilderField/index.tsx
|
|
353
|
-
import
|
|
354
|
-
import { useField as
|
|
516
|
+
import React6, { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
517
|
+
import { useField as useField3 } from "@payloadcms/ui";
|
|
355
518
|
|
|
356
519
|
// src/components/SchemaBuilderField/FieldRow.tsx
|
|
357
520
|
import React4, { useState as useState2 } from "react";
|
|
@@ -913,128 +1076,72 @@ function FieldRow({
|
|
|
913
1076
|
)))));
|
|
914
1077
|
}
|
|
915
1078
|
|
|
916
|
-
// src/components/
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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();
|
|
924
1092
|
}
|
|
925
|
-
|
|
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
|
+
);
|
|
926
1103
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
if (
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
setFields((prev) => [
|
|
949
|
-
...prev,
|
|
950
|
-
{ name: "", type: "text", label: "", required: false }
|
|
951
|
-
]);
|
|
952
|
-
}, []);
|
|
953
|
-
const updateField = useCallback3((index, updated) => {
|
|
954
|
-
setFields((prev) => prev.map((f, i) => i === index ? updated : f));
|
|
955
|
-
}, []);
|
|
956
|
-
const removeField = useCallback3((index) => {
|
|
957
|
-
setFields((prev) => prev.filter((_, i) => i !== index));
|
|
958
|
-
}, []);
|
|
959
|
-
const moveField = useCallback3((index, dir) => {
|
|
960
|
-
setFields((prev) => {
|
|
961
|
-
const next = [...prev];
|
|
962
|
-
const target = index + dir;
|
|
963
|
-
if (target < 0 || target >= next.length) return prev;
|
|
964
|
-
[next[index], next[target]] = [next[target], next[index]];
|
|
965
|
-
return next;
|
|
966
|
-
});
|
|
967
|
-
}, []);
|
|
968
|
-
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(
|
|
969
|
-
"span",
|
|
970
|
-
{
|
|
971
|
-
style: {
|
|
972
|
-
marginLeft: 8,
|
|
973
|
-
fontSize: 11,
|
|
974
|
-
color: "var(--theme-elevation-400)",
|
|
975
|
-
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 = [];
|
|
976
1125
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
index: i,
|
|
986
|
-
total: fields.length,
|
|
987
|
-
field,
|
|
988
|
-
onChange: (updated) => updateField(i, updated),
|
|
989
|
-
onRemove: () => removeField(i),
|
|
990
|
-
onMoveUp: i > 0 ? () => moveField(i, -1) : void 0,
|
|
991
|
-
onMoveDown: i < fields.length - 1 ? () => moveField(i, 1) : void 0,
|
|
992
|
-
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 = [];
|
|
993
1134
|
}
|
|
994
|
-
|
|
1135
|
+
currentFields = parentField.fields;
|
|
1136
|
+
}
|
|
1137
|
+
return currentFields;
|
|
995
1138
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
function EditInBuilderButton() {
|
|
1002
|
-
const { id } = useDocumentInfo();
|
|
1003
|
-
const { value: slug } = useField3({ path: "slug" });
|
|
1004
|
-
if (!id || !slug) return null;
|
|
1005
|
-
return /* @__PURE__ */ React6.createElement("div", { style: { marginTop: "1rem" } }, /* @__PURE__ */ React6.createElement(
|
|
1006
|
-
"a",
|
|
1007
|
-
{
|
|
1008
|
-
href: `/block-builder?load=${slug}`,
|
|
1009
|
-
target: "_blank",
|
|
1010
|
-
rel: "noopener noreferrer",
|
|
1011
|
-
style: {
|
|
1012
|
-
display: "inline-flex",
|
|
1013
|
-
alignItems: "center",
|
|
1014
|
-
gap: "6px",
|
|
1015
|
-
padding: "8px 16px",
|
|
1016
|
-
borderRadius: "6px",
|
|
1017
|
-
border: "1px solid #16a34a",
|
|
1018
|
-
color: "#16a34a",
|
|
1019
|
-
textDecoration: "none",
|
|
1020
|
-
fontSize: "13px",
|
|
1021
|
-
fontWeight: 500,
|
|
1022
|
-
background: "transparent",
|
|
1023
|
-
cursor: "pointer"
|
|
1024
|
-
}
|
|
1025
|
-
},
|
|
1026
|
-
"Edit in Block Builder"
|
|
1027
|
-
));
|
|
1139
|
+
function getTargetFields(block, parentPath) {
|
|
1140
|
+
return walkPath(block, parentPath, false);
|
|
1141
|
+
}
|
|
1142
|
+
function ensureTargetFields(block, parentPath) {
|
|
1143
|
+
return walkPath(block, parentPath, true);
|
|
1028
1144
|
}
|
|
1029
|
-
|
|
1030
|
-
// src/block-builder/components/canvas/BuilderShell.tsx
|
|
1031
|
-
import React16, { useCallback as useCallback5, useEffect as useEffect5, useState as useState8 } from "react";
|
|
1032
|
-
|
|
1033
|
-
// src/block-builder/store/builder.store.ts
|
|
1034
|
-
import { create } from "zustand";
|
|
1035
|
-
import { devtools, persist } from "zustand/middleware";
|
|
1036
|
-
import { immer } from "zustand/middleware/immer";
|
|
1037
|
-
import { v4 as uuidv4 } from "uuid";
|
|
1038
1145
|
function createDefaultField(type) {
|
|
1039
1146
|
const base = {
|
|
1040
1147
|
id: uuidv4(),
|
|
@@ -1045,7 +1152,6 @@ function createDefaultField(type) {
|
|
|
1045
1152
|
};
|
|
1046
1153
|
switch (type) {
|
|
1047
1154
|
case "select":
|
|
1048
|
-
case "radio":
|
|
1049
1155
|
return {
|
|
1050
1156
|
...base,
|
|
1051
1157
|
options: [
|
|
@@ -1054,15 +1160,31 @@ function createDefaultField(type) {
|
|
|
1054
1160
|
]
|
|
1055
1161
|
};
|
|
1056
1162
|
case "relationship":
|
|
1057
|
-
return { ...base,
|
|
1163
|
+
return { ...base, collection: "", hasMany: false };
|
|
1058
1164
|
case "array":
|
|
1059
1165
|
return { ...base, fields: [] };
|
|
1060
1166
|
case "group":
|
|
1061
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: [] }] };
|
|
1062
1174
|
default:
|
|
1063
1175
|
return base;
|
|
1064
1176
|
}
|
|
1065
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
|
+
}
|
|
1066
1188
|
function createDefaultBlock() {
|
|
1067
1189
|
return {
|
|
1068
1190
|
id: uuidv4(),
|
|
@@ -1086,11 +1208,13 @@ var useBuilderStore = create()(
|
|
|
1086
1208
|
isReadOnly: false,
|
|
1087
1209
|
loadedVersionId: null,
|
|
1088
1210
|
blockSlug: null,
|
|
1211
|
+
activeParentPath: [],
|
|
1089
1212
|
addBlock: () => set((state) => {
|
|
1090
1213
|
const block = createDefaultBlock();
|
|
1091
1214
|
state.blocks.push(block);
|
|
1092
1215
|
state.activeBlockId = block.id;
|
|
1093
1216
|
state.activeFieldId = null;
|
|
1217
|
+
state.activeParentPath = [];
|
|
1094
1218
|
state.isDirty = true;
|
|
1095
1219
|
}),
|
|
1096
1220
|
removeBlock: (blockId) => set((state) => {
|
|
@@ -1098,6 +1222,7 @@ var useBuilderStore = create()(
|
|
|
1098
1222
|
if (state.activeBlockId === blockId) {
|
|
1099
1223
|
state.activeBlockId = state.blocks[0]?.id ?? null;
|
|
1100
1224
|
state.activeFieldId = null;
|
|
1225
|
+
state.activeParentPath = [];
|
|
1101
1226
|
}
|
|
1102
1227
|
state.isDirty = true;
|
|
1103
1228
|
}),
|
|
@@ -1109,15 +1234,16 @@ var useBuilderStore = create()(
|
|
|
1109
1234
|
setActiveBlock: (blockId) => set((state) => {
|
|
1110
1235
|
state.activeBlockId = blockId;
|
|
1111
1236
|
state.activeFieldId = null;
|
|
1237
|
+
state.activeParentPath = [];
|
|
1112
1238
|
}),
|
|
1113
1239
|
duplicateBlock: (blockId) => set((state) => {
|
|
1114
1240
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1115
1241
|
if (!block) return;
|
|
1116
1242
|
const clone = JSON.parse(JSON.stringify(block));
|
|
1117
1243
|
clone.id = uuidv4();
|
|
1118
|
-
clone.slug = `${block.slug}
|
|
1244
|
+
clone.slug = `${block.slug}-copy`;
|
|
1119
1245
|
clone.interfaceName = block.interfaceName ? `${block.interfaceName}Copy` : void 0;
|
|
1120
|
-
clone.fields = clone.fields
|
|
1246
|
+
clone.fields = regenerateFieldIds(clone.fields);
|
|
1121
1247
|
const idx = state.blocks.findIndex((b) => b.id === blockId);
|
|
1122
1248
|
state.blocks.splice(idx + 1, 0, clone);
|
|
1123
1249
|
state.activeBlockId = clone.id;
|
|
@@ -1126,36 +1252,47 @@ var useBuilderStore = create()(
|
|
|
1126
1252
|
addField: (blockId, type) => set((state) => {
|
|
1127
1253
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1128
1254
|
if (!block) return;
|
|
1255
|
+
const targetFields = ensureTargetFields(block, state.activeParentPath);
|
|
1256
|
+
if (!targetFields) return;
|
|
1129
1257
|
const field = createDefaultField(type);
|
|
1130
|
-
|
|
1258
|
+
targetFields.push(field);
|
|
1131
1259
|
state.activeFieldId = field.id;
|
|
1132
1260
|
state.isDirty = true;
|
|
1133
1261
|
}),
|
|
1134
1262
|
removeField: (blockId, fieldId) => set((state) => {
|
|
1135
1263
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1136
1264
|
if (!block) return;
|
|
1137
|
-
|
|
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
|
+
}
|
|
1138
1271
|
if (state.activeFieldId === fieldId) state.activeFieldId = null;
|
|
1139
1272
|
state.isDirty = true;
|
|
1140
1273
|
}),
|
|
1141
1274
|
updateField: (blockId, fieldId, updates) => set((state) => {
|
|
1142
1275
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1143
1276
|
if (!block) return;
|
|
1144
|
-
const
|
|
1277
|
+
const targetFields = ensureTargetFields(block, state.activeParentPath);
|
|
1278
|
+
if (!targetFields) return;
|
|
1279
|
+
const field = targetFields.find((f) => f.id === fieldId);
|
|
1145
1280
|
if (field) Object.assign(field, updates);
|
|
1146
1281
|
state.isDirty = true;
|
|
1147
1282
|
}),
|
|
1148
1283
|
reorderFields: (blockId, fromIndex, toIndex) => set((state) => {
|
|
1149
1284
|
const block = state.blocks.find((b) => b.id === blockId);
|
|
1150
1285
|
if (!block) return;
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1286
|
+
const targetFields = ensureTargetFields(block, state.activeParentPath);
|
|
1287
|
+
if (!targetFields) return;
|
|
1288
|
+
const [moved] = targetFields.splice(fromIndex, 1);
|
|
1289
|
+
targetFields.splice(toIndex, 0, moved);
|
|
1153
1290
|
state.isDirty = true;
|
|
1154
1291
|
}),
|
|
1155
1292
|
setActiveField: (fieldId) => set((state) => {
|
|
1156
1293
|
state.activeFieldId = fieldId;
|
|
1157
1294
|
}),
|
|
1158
|
-
reset: () => set(() => ({ ...initialState, isReadOnly: false, loadedVersionId: null, blockSlug: null })),
|
|
1295
|
+
reset: () => set(() => ({ ...initialState, isReadOnly: false, loadedVersionId: null, blockSlug: null, activeParentPath: [] })),
|
|
1159
1296
|
markClean: () => set((state) => {
|
|
1160
1297
|
state.isDirty = false;
|
|
1161
1298
|
}),
|
|
@@ -1163,6 +1300,7 @@ var useBuilderStore = create()(
|
|
|
1163
1300
|
state.blocks = [block];
|
|
1164
1301
|
state.activeBlockId = block.id;
|
|
1165
1302
|
state.activeFieldId = null;
|
|
1303
|
+
state.activeParentPath = [];
|
|
1166
1304
|
state.isDirty = false;
|
|
1167
1305
|
}),
|
|
1168
1306
|
setVersionMeta: (versionId, isReadOnly) => set((state) => {
|
|
@@ -1171,10 +1309,36 @@ var useBuilderStore = create()(
|
|
|
1171
1309
|
}),
|
|
1172
1310
|
setBlockSlug: (slug) => set((state) => {
|
|
1173
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;
|
|
1174
1331
|
})
|
|
1175
1332
|
})),
|
|
1176
1333
|
{
|
|
1177
|
-
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
|
+
},
|
|
1178
1342
|
partialize: (state) => ({
|
|
1179
1343
|
blocks: state.blocks,
|
|
1180
1344
|
activeBlockId: state.activeBlockId
|
|
@@ -1184,163 +1348,191 @@ var useBuilderStore = create()(
|
|
|
1184
1348
|
)
|
|
1185
1349
|
);
|
|
1186
1350
|
|
|
1187
|
-
// src/block-builder/components/
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}).map(({ id: _id, relationTo, ...f }) => ({
|
|
1209
|
-
...f,
|
|
1210
|
-
type: TYPE_MAP[f.type] ?? f.type,
|
|
1211
|
-
// normalizer reads `collection`, block-builder stores `relationTo`
|
|
1212
|
-
...relationTo ? { collection: relationTo } : {}
|
|
1213
|
-
}));
|
|
1214
|
-
return {
|
|
1215
|
-
blockSlug: normalizeSlug(block.slug),
|
|
1216
|
-
name: block.labels?.singular ?? block.slug,
|
|
1217
|
-
schema: { fields },
|
|
1218
|
-
changelog: "Created via block builder"
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// src/block-builder/lib/codegen.ts
|
|
1223
|
-
function indent(n) {
|
|
1224
|
-
return " ".repeat(n);
|
|
1225
|
-
}
|
|
1226
|
-
function escStr(s) {
|
|
1227
|
-
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
1228
|
-
}
|
|
1229
|
-
function fieldToCode(field, depth = 1) {
|
|
1230
|
-
const pad = indent(depth);
|
|
1231
|
-
const innerPad = indent(depth + 1);
|
|
1232
|
-
const lines = [];
|
|
1233
|
-
lines.push(`${pad}name: '${escStr(field.name)}'`);
|
|
1234
|
-
lines.push(`${pad}type: '${field.type}'`);
|
|
1235
|
-
if (field.label) lines.push(`${pad}label: '${escStr(field.label)}'`);
|
|
1236
|
-
if (field.required) lines.push(`${pad}required: true`);
|
|
1237
|
-
if (field.unique) lines.push(`${pad}unique: true`);
|
|
1238
|
-
if (field.localized) lines.push(`${pad}localized: true`);
|
|
1239
|
-
if (field.defaultValue !== void 0) {
|
|
1240
|
-
const val = typeof field.defaultValue === "string" ? `'${escStr(String(field.defaultValue))}'` : field.defaultValue;
|
|
1241
|
-
lines.push(`${pad}defaultValue: ${val}`);
|
|
1242
|
-
}
|
|
1243
|
-
if (field.type === "richText") {
|
|
1244
|
-
lines.push(`${pad}editor: lexicalEditor({})`);
|
|
1245
|
-
}
|
|
1246
|
-
if (field.options && field.options.length > 0) {
|
|
1247
|
-
const opts = field.options.map((o) => `{ label: '${escStr(o.label)}', value: '${escStr(o.value)}' }`).join(`, `);
|
|
1248
|
-
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
|
+
};
|
|
1249
1372
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1373
|
+
static getDerivedStateFromError(error) {
|
|
1374
|
+
return { hasError: true, error };
|
|
1252
1375
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1376
|
+
componentDidCatch(error, errorInfo) {
|
|
1377
|
+
console.error("[Block Builder] Uncaught error:", error, errorInfo);
|
|
1255
1378
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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;
|
|
1275
1400
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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 {
|
|
1283
1411
|
}
|
|
1284
|
-
|
|
1285
|
-
${fieldToCode(f, 2)}
|
|
1286
|
-
}`).join(",\n");
|
|
1287
|
-
const labelsCode = block.labels ? `
|
|
1288
|
-
labels: {
|
|
1289
|
-
singular: '${escStr(block.labels.singular ?? block.slug)}',
|
|
1290
|
-
plural: '${escStr(block.labels.plural ?? block.slug + "s")}',
|
|
1291
|
-
},` : "";
|
|
1292
|
-
const interfaceLine = block.interfaceName ? `
|
|
1293
|
-
interfaceName: '${escStr(block.interfaceName)}',` : "";
|
|
1294
|
-
const exportName = block.interfaceName ?? toCamelCase(block.slug);
|
|
1295
|
-
return [
|
|
1296
|
-
imports.join("\n"),
|
|
1297
|
-
"",
|
|
1298
|
-
`export const ${exportName}: Block = {`,
|
|
1299
|
-
` slug: '${escStr(block.slug)}',${interfaceLine}${labelsCode}`,
|
|
1300
|
-
` fields: [`,
|
|
1301
|
-
fieldsCode,
|
|
1302
|
-
` ],`,
|
|
1303
|
-
`}`,
|
|
1304
|
-
""
|
|
1305
|
-
].join("\n");
|
|
1306
|
-
}
|
|
1307
|
-
function containsRichText(fields) {
|
|
1308
|
-
return fields.some(
|
|
1309
|
-
(f) => f.type === "richText" || (f.fields ? containsRichText(f.fields) : false)
|
|
1310
|
-
);
|
|
1311
|
-
}
|
|
1312
|
-
function toCamelCase(slug) {
|
|
1313
|
-
return slug.split(/[-_]/).map(
|
|
1314
|
-
(part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
|
|
1315
|
-
).join("");
|
|
1316
|
-
}
|
|
1317
|
-
function generateBlockOutput(block) {
|
|
1318
|
-
return {
|
|
1319
|
-
filename: `${block.slug}.ts`,
|
|
1320
|
-
code: generateBlockCode(block),
|
|
1321
|
-
language: "typescript"
|
|
1322
|
-
};
|
|
1412
|
+
return [];
|
|
1323
1413
|
}
|
|
1324
|
-
function
|
|
1325
|
-
|
|
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"))));
|
|
1326
1482
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
""
|
|
1338
|
-
|
|
1339
|
-
|
|
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
|
+
));
|
|
1340
1515
|
}
|
|
1341
1516
|
|
|
1517
|
+
// src/block-builder/components/canvas/BuilderShell.tsx
|
|
1518
|
+
import React18, { useCallback as useCallback5, useEffect as useEffect6, useState as useState9 } from "react";
|
|
1519
|
+
|
|
1342
1520
|
// src/block-builder/components/canvas/TopBar.tsx
|
|
1343
|
-
|
|
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 }) {
|
|
1344
1536
|
const blocks = useBuilderStore((s) => s.blocks);
|
|
1345
1537
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1346
1538
|
const activeBlock = blocks.find((b) => b.id === activeBlockId);
|
|
@@ -1348,21 +1540,23 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1348
1540
|
const isDirty = useBuilderStore((s) => s.isDirty);
|
|
1349
1541
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
1350
1542
|
const setVersionMeta = useBuilderStore((s) => s.setVersionMeta);
|
|
1351
|
-
const
|
|
1543
|
+
const loadBlock = useBuilderStore((s) => s.loadBlock);
|
|
1544
|
+
const setNotification = onSetNotification;
|
|
1352
1545
|
const [versionDropdownOpen, setVersionDropdownOpen] = useState4(false);
|
|
1353
1546
|
const [blockPickerOpen, setBlockPickerOpen] = useState4(false);
|
|
1354
|
-
const dropdownRef =
|
|
1355
|
-
const blockPickerRef =
|
|
1547
|
+
const dropdownRef = useRef4(null);
|
|
1548
|
+
const blockPickerRef = useRef4(null);
|
|
1549
|
+
const fileInputRef = useRef4(null);
|
|
1356
1550
|
const selectedVersion = versions.find((v) => v.id === selectedVersionId);
|
|
1357
1551
|
const currentVersion = versions.find((v) => v.isCurrent);
|
|
1358
1552
|
const activeBlockDef = blockDefs.find((b) => b.slug === activeSlug);
|
|
1359
|
-
|
|
1553
|
+
useEffect4(() => {
|
|
1360
1554
|
if (notification?.status === "success") {
|
|
1361
1555
|
const t = setTimeout(() => setNotification(null), 3e3);
|
|
1362
1556
|
return () => clearTimeout(t);
|
|
1363
1557
|
}
|
|
1364
1558
|
}, [notification]);
|
|
1365
|
-
|
|
1559
|
+
useEffect4(() => {
|
|
1366
1560
|
function handleClick(e) {
|
|
1367
1561
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
|
1368
1562
|
setVersionDropdownOpen(false);
|
|
@@ -1378,10 +1572,17 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1378
1572
|
if (!activeBlock || isReadOnly) return false;
|
|
1379
1573
|
setNotification({ status: "publishing" });
|
|
1380
1574
|
try {
|
|
1381
|
-
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
|
+
};
|
|
1382
1583
|
const res = await fetch("/api/blocks/save", {
|
|
1383
1584
|
method: "POST",
|
|
1384
|
-
headers: { "Content-Type": "application/json" },
|
|
1585
|
+
headers: { "Content-Type": "application/json", "X-Block-Builder": "1" },
|
|
1385
1586
|
body: JSON.stringify(req)
|
|
1386
1587
|
});
|
|
1387
1588
|
const json = await res.json();
|
|
@@ -1392,7 +1593,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1392
1593
|
status: "success",
|
|
1393
1594
|
msg: `v${json.versionNumber ?? "?"} published successfully!`
|
|
1394
1595
|
});
|
|
1395
|
-
onAfterPublish();
|
|
1596
|
+
onAfterPublish(publishedSlug);
|
|
1396
1597
|
return true;
|
|
1397
1598
|
} else {
|
|
1398
1599
|
setNotification({
|
|
@@ -1411,35 +1612,54 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1411
1612
|
return false;
|
|
1412
1613
|
}
|
|
1413
1614
|
}
|
|
1414
|
-
function
|
|
1415
|
-
if (
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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 = "";
|
|
1428
1648
|
}
|
|
1429
1649
|
function formatDate(iso) {
|
|
1430
1650
|
return new Date(iso).toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
1431
1651
|
}
|
|
1432
|
-
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(
|
|
1433
1653
|
"button",
|
|
1434
1654
|
{
|
|
1435
1655
|
type: "button",
|
|
1436
1656
|
className: "bb-block-picker__trigger",
|
|
1437
1657
|
onClick: () => setBlockPickerOpen((o) => !o)
|
|
1438
1658
|
},
|
|
1439
|
-
/* @__PURE__ */
|
|
1440
|
-
/* @__PURE__ */
|
|
1441
|
-
/* @__PURE__ */
|
|
1442
|
-
), 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(
|
|
1443
1663
|
"button",
|
|
1444
1664
|
{
|
|
1445
1665
|
key: b.id,
|
|
@@ -1450,20 +1670,20 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1450
1670
|
onBlockSelect(b.slug);
|
|
1451
1671
|
}
|
|
1452
1672
|
},
|
|
1453
|
-
/* @__PURE__ */
|
|
1454
|
-
/* @__PURE__ */
|
|
1455
|
-
)))), 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(
|
|
1456
1676
|
"button",
|
|
1457
1677
|
{
|
|
1458
1678
|
type: "button",
|
|
1459
1679
|
className: `bb-version-selector__trigger${isReadOnly ? " bb-version-selector__trigger--readonly" : ""}`,
|
|
1460
1680
|
onClick: () => setVersionDropdownOpen((o) => !o)
|
|
1461
1681
|
},
|
|
1462
|
-
/* @__PURE__ */
|
|
1463
|
-
/* @__PURE__ */
|
|
1464
|
-
selectedVersion?.isCurrent && /* @__PURE__ */
|
|
1465
|
-
/* @__PURE__ */
|
|
1466
|
-
), 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(
|
|
1467
1687
|
"button",
|
|
1468
1688
|
{
|
|
1469
1689
|
key: v.id,
|
|
@@ -1474,20 +1694,45 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1474
1694
|
onVersionSelect(v.id);
|
|
1475
1695
|
}
|
|
1476
1696
|
},
|
|
1477
|
-
/* @__PURE__ */
|
|
1478
|
-
/* @__PURE__ */
|
|
1479
|
-
/* @__PURE__ */
|
|
1480
|
-
))))), /* @__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(
|
|
1710
|
+
"button",
|
|
1711
|
+
{
|
|
1712
|
+
type: "button",
|
|
1713
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1714
|
+
className: "bb-btn bb-btn--secondary"
|
|
1715
|
+
},
|
|
1716
|
+
"Import JSON"
|
|
1717
|
+
), /* @__PURE__ */ React8.createElement(
|
|
1481
1718
|
"button",
|
|
1482
1719
|
{
|
|
1483
1720
|
type: "button",
|
|
1484
|
-
onClick:
|
|
1485
|
-
disabled:
|
|
1486
|
-
className: "bb-btn bb-btn--secondary"
|
|
1487
|
-
style: { display: "none" }
|
|
1721
|
+
onClick: handleExportJson,
|
|
1722
|
+
disabled: !activeBlock,
|
|
1723
|
+
className: "bb-btn bb-btn--secondary"
|
|
1488
1724
|
},
|
|
1489
|
-
"Export
|
|
1490
|
-
),
|
|
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"}`
|
|
1733
|
+
},
|
|
1734
|
+
previewOpen ? "Close Preview" : "Live Preview"
|
|
1735
|
+
), isReadOnly ? /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(
|
|
1491
1736
|
"button",
|
|
1492
1737
|
{
|
|
1493
1738
|
type: "button",
|
|
@@ -1498,7 +1743,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1498
1743
|
disabled: !currentVersion
|
|
1499
1744
|
},
|
|
1500
1745
|
"Back to current"
|
|
1501
|
-
), /* @__PURE__ */
|
|
1746
|
+
), /* @__PURE__ */ React8.createElement(
|
|
1502
1747
|
"button",
|
|
1503
1748
|
{
|
|
1504
1749
|
type: "button",
|
|
@@ -1510,7 +1755,7 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1510
1755
|
className: "bb-btn bb-btn--warning"
|
|
1511
1756
|
},
|
|
1512
1757
|
notification?.status === "publishing" ? "Restoring..." : "Restore as new version"
|
|
1513
|
-
)) : /* @__PURE__ */
|
|
1758
|
+
)) : /* @__PURE__ */ React8.createElement(
|
|
1514
1759
|
"button",
|
|
1515
1760
|
{
|
|
1516
1761
|
type: "button",
|
|
@@ -1519,37 +1764,55 @@ function TopBar({ blockDefs, activeSlug, onBlockSelect, versions, selectedVersio
|
|
|
1519
1764
|
className: "bb-btn bb-btn--primary"
|
|
1520
1765
|
},
|
|
1521
1766
|
notification?.status === "publishing" ? "Publishing..." : "Publish to Payload"
|
|
1522
|
-
))), 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 })))));
|
|
1523
1768
|
}
|
|
1524
1769
|
|
|
1525
1770
|
// src/block-builder/components/canvas/BlockList.tsx
|
|
1526
|
-
import
|
|
1771
|
+
import React9 from "react";
|
|
1527
1772
|
import { Copy, Trash2, Plus } from "lucide-react";
|
|
1528
|
-
function BlockList() {
|
|
1773
|
+
function BlockList({ blockDefs = [], activeSlug, onBlockSelect }) {
|
|
1529
1774
|
const blocks = useBuilderStore((s) => s.blocks);
|
|
1530
1775
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1531
1776
|
const addBlock = useBuilderStore((s) => s.addBlock);
|
|
1532
1777
|
const removeBlock = useBuilderStore((s) => s.removeBlock);
|
|
1533
1778
|
const duplicateBlock = useBuilderStore((s) => s.duplicateBlock);
|
|
1534
1779
|
const setActiveBlock = useBuilderStore((s) => s.setActiveBlock);
|
|
1535
|
-
|
|
1780
|
+
const useApiNav = blockDefs.length > 0;
|
|
1781
|
+
const loadedBlock = useApiNav ? blocks.find((b) => b.slug === activeSlug) : null;
|
|
1782
|
+
const localOnlyBlocks = blocks.filter(
|
|
1783
|
+
(b) => !blockDefs.some((d) => d.slug === b.slug)
|
|
1784
|
+
);
|
|
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) => {
|
|
1786
|
+
const isActive = def.slug === activeSlug;
|
|
1787
|
+
const fieldCount = isActive && loadedBlock ? loadedBlock.fields.length : null;
|
|
1788
|
+
return /* @__PURE__ */ React9.createElement(
|
|
1789
|
+
"div",
|
|
1790
|
+
{
|
|
1791
|
+
key: def.id,
|
|
1792
|
+
onClick: () => onBlockSelect?.(def.slug),
|
|
1793
|
+
className: `bb-block-item${isActive ? " bb-block-item--active" : ""}`
|
|
1794
|
+
},
|
|
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)
|
|
1797
|
+
);
|
|
1798
|
+
}), localOnlyBlocks.map((block) => {
|
|
1536
1799
|
const isActive = block.id === activeBlockId;
|
|
1537
|
-
return /* @__PURE__ */
|
|
1800
|
+
return /* @__PURE__ */ React9.createElement(
|
|
1538
1801
|
"div",
|
|
1539
1802
|
{
|
|
1540
1803
|
key: block.id,
|
|
1541
1804
|
onClick: () => setActiveBlock(block.id),
|
|
1542
1805
|
className: `bb-block-item${isActive ? " bb-block-item--active" : ""}`
|
|
1543
1806
|
},
|
|
1544
|
-
/* @__PURE__ */
|
|
1545
|
-
/* @__PURE__ */
|
|
1546
|
-
/* @__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(
|
|
1547
1810
|
"div",
|
|
1548
1811
|
{
|
|
1549
1812
|
className: "bb-block-item__actions",
|
|
1550
1813
|
onClick: (e) => e.stopPropagation()
|
|
1551
1814
|
},
|
|
1552
|
-
/* @__PURE__ */
|
|
1815
|
+
/* @__PURE__ */ React9.createElement(
|
|
1553
1816
|
"button",
|
|
1554
1817
|
{
|
|
1555
1818
|
type: "button",
|
|
@@ -1557,9 +1820,9 @@ function BlockList() {
|
|
|
1557
1820
|
className: "bb-block-action",
|
|
1558
1821
|
title: "Duplicate"
|
|
1559
1822
|
},
|
|
1560
|
-
/* @__PURE__ */
|
|
1823
|
+
/* @__PURE__ */ React9.createElement(Copy, { size: 12, strokeWidth: 1.75 })
|
|
1561
1824
|
),
|
|
1562
|
-
/* @__PURE__ */
|
|
1825
|
+
/* @__PURE__ */ React9.createElement(
|
|
1563
1826
|
"button",
|
|
1564
1827
|
{
|
|
1565
1828
|
type: "button",
|
|
@@ -1567,15 +1830,15 @@ function BlockList() {
|
|
|
1567
1830
|
className: "bb-block-action bb-block-action--danger",
|
|
1568
1831
|
title: "Delete"
|
|
1569
1832
|
},
|
|
1570
|
-
/* @__PURE__ */
|
|
1833
|
+
/* @__PURE__ */ React9.createElement(Trash2, { size: 12, strokeWidth: 1.75 })
|
|
1571
1834
|
)
|
|
1572
1835
|
)
|
|
1573
1836
|
);
|
|
1574
|
-
})));
|
|
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.")));
|
|
1575
1838
|
}
|
|
1576
1839
|
|
|
1577
1840
|
// src/block-builder/components/canvas/BuilderCanvas.tsx
|
|
1578
|
-
import
|
|
1841
|
+
import React11 from "react";
|
|
1579
1842
|
import {
|
|
1580
1843
|
DndContext,
|
|
1581
1844
|
closestCenter,
|
|
@@ -1592,68 +1855,107 @@ import {
|
|
|
1592
1855
|
import { restrictToVerticalAxis, restrictToParentElement } from "@dnd-kit/modifiers";
|
|
1593
1856
|
|
|
1594
1857
|
// src/block-builder/components/canvas/SortableFieldCard.tsx
|
|
1595
|
-
import
|
|
1858
|
+
import React10 from "react";
|
|
1596
1859
|
import { useSortable } from "@dnd-kit/sortable";
|
|
1597
1860
|
import { CSS } from "@dnd-kit/utilities";
|
|
1598
1861
|
import {
|
|
1599
1862
|
Type,
|
|
1600
1863
|
AlignLeft,
|
|
1864
|
+
AlignJustify,
|
|
1601
1865
|
Hash,
|
|
1602
1866
|
Mail,
|
|
1603
1867
|
Calendar,
|
|
1604
1868
|
CheckSquare,
|
|
1605
1869
|
ChevronDown as ChevronDown2,
|
|
1606
|
-
Circle,
|
|
1607
1870
|
Upload,
|
|
1871
|
+
Image,
|
|
1608
1872
|
Link,
|
|
1609
1873
|
Braces,
|
|
1874
|
+
List,
|
|
1875
|
+
Box,
|
|
1876
|
+
Columns,
|
|
1877
|
+
Folder,
|
|
1610
1878
|
X as X2
|
|
1611
1879
|
} from "lucide-react";
|
|
1612
1880
|
var ICON_MAP = {
|
|
1613
1881
|
text: Type,
|
|
1614
1882
|
textarea: AlignLeft,
|
|
1883
|
+
richtext: AlignJustify,
|
|
1615
1884
|
number: Hash,
|
|
1616
1885
|
email: Mail,
|
|
1617
1886
|
date: Calendar,
|
|
1618
1887
|
checkbox: CheckSquare,
|
|
1619
1888
|
select: ChevronDown2,
|
|
1620
|
-
|
|
1621
|
-
|
|
1889
|
+
image: Image,
|
|
1890
|
+
file: Upload,
|
|
1622
1891
|
relationship: Link,
|
|
1623
|
-
json: Braces
|
|
1892
|
+
json: Braces,
|
|
1893
|
+
array: List,
|
|
1894
|
+
group: Box,
|
|
1895
|
+
row: Columns,
|
|
1896
|
+
tabs: Folder,
|
|
1897
|
+
collapsible: ChevronDown2
|
|
1624
1898
|
};
|
|
1625
|
-
|
|
1899
|
+
var SortableFieldCard = React10.memo(function SortableFieldCard2({
|
|
1900
|
+
field,
|
|
1901
|
+
blockId,
|
|
1902
|
+
index,
|
|
1903
|
+
isActive,
|
|
1904
|
+
isReadOnly,
|
|
1905
|
+
onDrillDown
|
|
1906
|
+
}) {
|
|
1626
1907
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
1627
1908
|
id: field.id
|
|
1628
1909
|
});
|
|
1629
|
-
const activeFieldId = useBuilderStore((s) => s.activeFieldId);
|
|
1630
1910
|
const setActiveField = useBuilderStore((s) => s.setActiveField);
|
|
1631
1911
|
const removeField = useBuilderStore((s) => s.removeField);
|
|
1632
|
-
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
1633
|
-
const isActive = activeFieldId === field.id;
|
|
1634
1912
|
const wrapStyle = {
|
|
1635
1913
|
transform: CSS.Transform.toString(transform),
|
|
1636
1914
|
transition
|
|
1637
1915
|
};
|
|
1638
|
-
return /* @__PURE__ */
|
|
1916
|
+
return /* @__PURE__ */ React10.createElement(
|
|
1639
1917
|
"div",
|
|
1640
1918
|
{
|
|
1641
1919
|
ref: setNodeRef,
|
|
1642
1920
|
style: wrapStyle,
|
|
1643
1921
|
className: `bb-field-card-wrap${isDragging ? " bb-field-card-wrap--dragging" : ""}`,
|
|
1644
1922
|
...attributes,
|
|
1645
|
-
...listeners
|
|
1923
|
+
...listeners,
|
|
1924
|
+
"aria-label": `Draggable field card for ${field.name || "unnamed"}`
|
|
1646
1925
|
},
|
|
1647
|
-
/* @__PURE__ */
|
|
1926
|
+
/* @__PURE__ */ React10.createElement(
|
|
1648
1927
|
"div",
|
|
1649
1928
|
{
|
|
1650
1929
|
className: `bb-field-card${isActive ? " bb-field-card--active" : ""}`,
|
|
1651
1930
|
onClick: () => setActiveField(isActive ? null : field.id)
|
|
1652
1931
|
},
|
|
1653
|
-
/* @__PURE__ */
|
|
1654
|
-
/* @__PURE__ */
|
|
1655
|
-
/* @__PURE__ */
|
|
1656
|
-
|
|
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(
|
|
1657
1959
|
"button",
|
|
1658
1960
|
{
|
|
1659
1961
|
type: "button",
|
|
@@ -1665,21 +1967,46 @@ function SortableFieldCard({ field, blockId, index }) {
|
|
|
1665
1967
|
className: "bb-field-card__delete",
|
|
1666
1968
|
title: "Remove field"
|
|
1667
1969
|
},
|
|
1668
|
-
/* @__PURE__ */
|
|
1970
|
+
/* @__PURE__ */ React10.createElement(X2, { size: 12, strokeWidth: 2 })
|
|
1669
1971
|
)
|
|
1670
1972
|
)
|
|
1671
1973
|
);
|
|
1672
|
-
}
|
|
1974
|
+
});
|
|
1673
1975
|
function FieldIcon({ type }) {
|
|
1674
1976
|
const Icon = ICON_MAP[type];
|
|
1675
|
-
return Icon ? /* @__PURE__ */
|
|
1977
|
+
return Icon ? /* @__PURE__ */ React10.createElement(Icon, { size: 13, strokeWidth: 1.75 }) : null;
|
|
1676
1978
|
}
|
|
1677
1979
|
|
|
1678
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
|
+
}
|
|
1679
2001
|
function BuilderCanvas() {
|
|
1680
2002
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1681
2003
|
const block = useBuilderStore((s) => s.blocks.find((b) => b.id === activeBlockId));
|
|
1682
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);
|
|
1683
2010
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
1684
2011
|
const sensors = useSensors(
|
|
1685
2012
|
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
@@ -1689,16 +2016,41 @@ function BuilderCanvas() {
|
|
|
1689
2016
|
if (isReadOnly) return;
|
|
1690
2017
|
const { active, over } = event;
|
|
1691
2018
|
if (!over || active.id === over.id || !block) return;
|
|
1692
|
-
const
|
|
1693
|
-
|
|
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);
|
|
1694
2023
|
if (fromIndex !== -1 && toIndex !== -1) {
|
|
1695
2024
|
reorderFields(block.id, fromIndex, toIndex);
|
|
1696
2025
|
}
|
|
1697
2026
|
}
|
|
1698
2027
|
if (!block) {
|
|
1699
|
-
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."));
|
|
1700
2029
|
}
|
|
1701
|
-
|
|
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(
|
|
1702
2054
|
DndContext,
|
|
1703
2055
|
{
|
|
1704
2056
|
sensors,
|
|
@@ -1706,19 +2058,22 @@ function BuilderCanvas() {
|
|
|
1706
2058
|
modifiers: [restrictToVerticalAxis, restrictToParentElement],
|
|
1707
2059
|
onDragEnd: handleDragEnd
|
|
1708
2060
|
},
|
|
1709
|
-
/* @__PURE__ */
|
|
2061
|
+
/* @__PURE__ */ React11.createElement(
|
|
1710
2062
|
SortableContext,
|
|
1711
2063
|
{
|
|
1712
|
-
items:
|
|
2064
|
+
items: targetFields.map((f) => f.id),
|
|
1713
2065
|
strategy: verticalListSortingStrategy
|
|
1714
2066
|
},
|
|
1715
|
-
/* @__PURE__ */
|
|
2067
|
+
/* @__PURE__ */ React11.createElement("div", { className: "bb-canvas__field-list" }, targetFields.map((field, i) => /* @__PURE__ */ React11.createElement(
|
|
1716
2068
|
SortableFieldCard,
|
|
1717
2069
|
{
|
|
1718
2070
|
key: field.id,
|
|
1719
2071
|
field,
|
|
1720
2072
|
blockId: block.id,
|
|
1721
|
-
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
|
|
1722
2077
|
}
|
|
1723
2078
|
)))
|
|
1724
2079
|
)
|
|
@@ -1726,18 +2081,18 @@ function BuilderCanvas() {
|
|
|
1726
2081
|
}
|
|
1727
2082
|
|
|
1728
2083
|
// src/block-builder/components/canvas/ConfigPanel.tsx
|
|
1729
|
-
import
|
|
2084
|
+
import React14, { useState as useState5 } from "react";
|
|
1730
2085
|
|
|
1731
2086
|
// src/block-builder/components/config/BlockConfig.tsx
|
|
1732
|
-
import
|
|
2087
|
+
import React12 from "react";
|
|
1733
2088
|
function BlockConfig() {
|
|
1734
2089
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1735
2090
|
const block = useBuilderStore((s) => s.blocks.find((b) => b.id === activeBlockId));
|
|
1736
2091
|
const updateBlock = useBuilderStore((s) => s.updateBlock);
|
|
1737
2092
|
if (!block) {
|
|
1738
|
-
return /* @__PURE__ */
|
|
2093
|
+
return /* @__PURE__ */ React12.createElement("div", { className: "bb-form__empty" }, "No block selected.");
|
|
1739
2094
|
}
|
|
1740
|
-
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(
|
|
1741
2096
|
"input",
|
|
1742
2097
|
{
|
|
1743
2098
|
type: "text",
|
|
@@ -1746,7 +2101,7 @@ function BlockConfig() {
|
|
|
1746
2101
|
placeholder: "myBlock",
|
|
1747
2102
|
className: "bb-input"
|
|
1748
2103
|
}
|
|
1749
|
-
), /* @__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(
|
|
1750
2105
|
"input",
|
|
1751
2106
|
{
|
|
1752
2107
|
type: "text",
|
|
@@ -1755,7 +2110,7 @@ function BlockConfig() {
|
|
|
1755
2110
|
placeholder: "MyBlock",
|
|
1756
2111
|
className: "bb-input"
|
|
1757
2112
|
}
|
|
1758
|
-
)), /* @__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(
|
|
1759
2114
|
"input",
|
|
1760
2115
|
{
|
|
1761
2116
|
type: "text",
|
|
@@ -1764,7 +2119,7 @@ function BlockConfig() {
|
|
|
1764
2119
|
placeholder: "My Block",
|
|
1765
2120
|
className: "bb-input"
|
|
1766
2121
|
}
|
|
1767
|
-
)), /* @__PURE__ */
|
|
2122
|
+
)), /* @__PURE__ */ React12.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React12.createElement("label", { className: "bb-form__label" }, "Plural Label"), /* @__PURE__ */ React12.createElement(
|
|
1768
2123
|
"input",
|
|
1769
2124
|
{
|
|
1770
2125
|
type: "text",
|
|
@@ -1773,7 +2128,7 @@ function BlockConfig() {
|
|
|
1773
2128
|
placeholder: "My Blocks",
|
|
1774
2129
|
className: "bb-input"
|
|
1775
2130
|
}
|
|
1776
|
-
))), /* @__PURE__ */
|
|
2131
|
+
))), /* @__PURE__ */ React12.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React12.createElement("label", { className: "bb-form__label" }, "Image URL"), /* @__PURE__ */ React12.createElement(
|
|
1777
2132
|
"input",
|
|
1778
2133
|
{
|
|
1779
2134
|
type: "text",
|
|
@@ -1782,38 +2137,62 @@ function BlockConfig() {
|
|
|
1782
2137
|
placeholder: "https://...",
|
|
1783
2138
|
className: "bb-input"
|
|
1784
2139
|
}
|
|
1785
|
-
)), /* @__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"));
|
|
1786
2141
|
}
|
|
1787
2142
|
|
|
1788
2143
|
// src/block-builder/components/config/FieldConfig.tsx
|
|
1789
|
-
import
|
|
2144
|
+
import React13 from "react";
|
|
1790
2145
|
var ALL_TYPES = [
|
|
1791
2146
|
"text",
|
|
1792
2147
|
"textarea",
|
|
2148
|
+
"richtext",
|
|
1793
2149
|
"number",
|
|
1794
2150
|
"email",
|
|
2151
|
+
"url",
|
|
2152
|
+
"color",
|
|
1795
2153
|
"date",
|
|
1796
2154
|
"checkbox",
|
|
1797
2155
|
"select",
|
|
1798
|
-
"
|
|
1799
|
-
"
|
|
2156
|
+
"multiselect",
|
|
2157
|
+
"image",
|
|
2158
|
+
"file",
|
|
2159
|
+
"relationship",
|
|
2160
|
+
"json",
|
|
2161
|
+
"array",
|
|
2162
|
+
"group",
|
|
2163
|
+
"blocks",
|
|
2164
|
+
"row",
|
|
2165
|
+
"tabs",
|
|
2166
|
+
"collapsible"
|
|
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",
|
|
1800
2177
|
"relationship",
|
|
1801
2178
|
"json"
|
|
1802
|
-
];
|
|
2179
|
+
]);
|
|
1803
2180
|
function FieldConfig() {
|
|
1804
2181
|
const activeBlockId = useBuilderStore((s) => s.activeBlockId);
|
|
1805
2182
|
const activeFieldId = useBuilderStore((s) => s.activeFieldId);
|
|
2183
|
+
const activeParentPath = useBuilderStore((s) => s.activeParentPath);
|
|
1806
2184
|
const block = useBuilderStore((s) => s.blocks.find((b) => b.id === activeBlockId));
|
|
1807
|
-
const field = block?.
|
|
2185
|
+
const field = block ? getTargetFields(block, activeParentPath)?.find((f) => f.id === activeFieldId) : void 0;
|
|
1808
2186
|
const updateField = useBuilderStore((s) => s.updateField);
|
|
2187
|
+
const pushParentPath = useBuilderStore((s) => s.pushParentPath);
|
|
1809
2188
|
if (!activeBlockId || !activeFieldId || !field) {
|
|
1810
|
-
return /* @__PURE__ */
|
|
2189
|
+
return /* @__PURE__ */ React13.createElement("div", { className: "bb-form__empty" }, "Select a field to configure it.");
|
|
1811
2190
|
}
|
|
1812
2191
|
function upd(updates) {
|
|
1813
2192
|
updateField(activeBlockId, activeFieldId, updates);
|
|
1814
2193
|
}
|
|
1815
|
-
const needsOptions = field.type === "select" || field.type === "
|
|
1816
|
-
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(
|
|
1817
2196
|
"input",
|
|
1818
2197
|
{
|
|
1819
2198
|
type: "text",
|
|
@@ -1822,15 +2201,15 @@ function FieldConfig() {
|
|
|
1822
2201
|
placeholder: "fieldName",
|
|
1823
2202
|
className: "bb-input"
|
|
1824
2203
|
}
|
|
1825
|
-
)), /* @__PURE__ */
|
|
2204
|
+
)), /* @__PURE__ */ React13.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React13.createElement("label", { className: "bb-form__label" }, "Type *"), /* @__PURE__ */ React13.createElement(
|
|
1826
2205
|
"select",
|
|
1827
2206
|
{
|
|
1828
2207
|
value: field.type,
|
|
1829
2208
|
onChange: (e) => upd({ type: e.target.value }),
|
|
1830
2209
|
className: "bb-input bb-select"
|
|
1831
2210
|
},
|
|
1832
|
-
ALL_TYPES.map((t) => /* @__PURE__ */
|
|
1833
|
-
))), /* @__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(
|
|
1834
2213
|
"input",
|
|
1835
2214
|
{
|
|
1836
2215
|
type: "text",
|
|
@@ -1839,28 +2218,44 @@ function FieldConfig() {
|
|
|
1839
2218
|
placeholder: "Human-readable label",
|
|
1840
2219
|
className: "bb-input"
|
|
1841
2220
|
}
|
|
1842
|
-
)), /* @__PURE__ */
|
|
2221
|
+
)), /* @__PURE__ */ React13.createElement("div", { className: "bb-flags-row" }, /* @__PURE__ */ React13.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ React13.createElement(
|
|
1843
2222
|
"input",
|
|
1844
2223
|
{
|
|
1845
2224
|
type: "checkbox",
|
|
1846
2225
|
checked: field.required ?? false,
|
|
1847
2226
|
onChange: (e) => upd({ required: e.target.checked })
|
|
1848
2227
|
}
|
|
1849
|
-
), "Required"), /* @__PURE__ */
|
|
2228
|
+
), "Required"), /* @__PURE__ */ React13.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ React13.createElement(
|
|
1850
2229
|
"input",
|
|
1851
2230
|
{
|
|
1852
2231
|
type: "checkbox",
|
|
1853
2232
|
checked: field.unique ?? false,
|
|
1854
2233
|
onChange: (e) => upd({ unique: e.target.checked })
|
|
1855
2234
|
}
|
|
1856
|
-
), "Unique"), /* @__PURE__ */
|
|
2235
|
+
), "Unique"), /* @__PURE__ */ React13.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ React13.createElement(
|
|
1857
2236
|
"input",
|
|
1858
2237
|
{
|
|
1859
2238
|
type: "checkbox",
|
|
1860
2239
|
checked: field.localized ?? false,
|
|
1861
2240
|
onChange: (e) => upd({ localized: e.target.checked })
|
|
1862
2241
|
}
|
|
1863
|
-
), "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(
|
|
1864
2259
|
"input",
|
|
1865
2260
|
{
|
|
1866
2261
|
type: "text",
|
|
@@ -1869,23 +2264,23 @@ function FieldConfig() {
|
|
|
1869
2264
|
placeholder: "Default value",
|
|
1870
2265
|
className: "bb-input"
|
|
1871
2266
|
}
|
|
1872
|
-
)), 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(
|
|
1873
2268
|
"input",
|
|
1874
2269
|
{
|
|
1875
2270
|
type: "text",
|
|
1876
|
-
value: field.
|
|
1877
|
-
onChange: (e) => upd({
|
|
2271
|
+
value: field.collection ?? "",
|
|
2272
|
+
onChange: (e) => upd({ collection: e.target.value }),
|
|
1878
2273
|
placeholder: "pages",
|
|
1879
2274
|
className: "bb-input"
|
|
1880
2275
|
}
|
|
1881
|
-
)), /* @__PURE__ */
|
|
2276
|
+
)), /* @__PURE__ */ React13.createElement("label", { className: "bb-checkbox-row" }, /* @__PURE__ */ React13.createElement(
|
|
1882
2277
|
"input",
|
|
1883
2278
|
{
|
|
1884
2279
|
type: "checkbox",
|
|
1885
2280
|
checked: field.hasMany ?? false,
|
|
1886
2281
|
onChange: (e) => upd({ hasMany: e.target.checked })
|
|
1887
2282
|
}
|
|
1888
|
-
), "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(
|
|
1889
2284
|
"input",
|
|
1890
2285
|
{
|
|
1891
2286
|
type: "number",
|
|
@@ -1893,7 +2288,7 @@ function FieldConfig() {
|
|
|
1893
2288
|
onChange: (e) => upd({ minRows: e.target.value === "" ? void 0 : e.target.valueAsNumber }),
|
|
1894
2289
|
className: "bb-input"
|
|
1895
2290
|
}
|
|
1896
|
-
)), /* @__PURE__ */
|
|
2291
|
+
)), /* @__PURE__ */ React13.createElement("div", { className: "bb-form__section" }, /* @__PURE__ */ React13.createElement("label", { className: "bb-form__label" }, "Max Rows"), /* @__PURE__ */ React13.createElement(
|
|
1897
2292
|
"input",
|
|
1898
2293
|
{
|
|
1899
2294
|
type: "number",
|
|
@@ -1901,7 +2296,54 @@ function FieldConfig() {
|
|
|
1901
2296
|
onChange: (e) => upd({ maxRows: e.target.value === "" ? void 0 : e.target.valueAsNumber }),
|
|
1902
2297
|
className: "bb-input"
|
|
1903
2298
|
}
|
|
1904
|
-
))),
|
|
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(
|
|
1905
2347
|
"input",
|
|
1906
2348
|
{
|
|
1907
2349
|
type: "text",
|
|
@@ -1914,7 +2356,7 @@ function FieldConfig() {
|
|
|
1914
2356
|
},
|
|
1915
2357
|
className: "bb-input"
|
|
1916
2358
|
}
|
|
1917
|
-
), /* @__PURE__ */
|
|
2359
|
+
), /* @__PURE__ */ React13.createElement(
|
|
1918
2360
|
"input",
|
|
1919
2361
|
{
|
|
1920
2362
|
type: "text",
|
|
@@ -1927,7 +2369,7 @@ function FieldConfig() {
|
|
|
1927
2369
|
},
|
|
1928
2370
|
className: "bb-input"
|
|
1929
2371
|
}
|
|
1930
|
-
), /* @__PURE__ */
|
|
2372
|
+
), /* @__PURE__ */ React13.createElement(
|
|
1931
2373
|
"button",
|
|
1932
2374
|
{
|
|
1933
2375
|
type: "button",
|
|
@@ -1939,7 +2381,7 @@ function FieldConfig() {
|
|
|
1939
2381
|
title: "Remove option"
|
|
1940
2382
|
},
|
|
1941
2383
|
"x"
|
|
1942
|
-
))), /* @__PURE__ */
|
|
2384
|
+
))), /* @__PURE__ */ React13.createElement(
|
|
1943
2385
|
"button",
|
|
1944
2386
|
{
|
|
1945
2387
|
type: "button",
|
|
@@ -1956,7 +2398,7 @@ function ConfigPanel() {
|
|
|
1956
2398
|
const activeFieldId = useBuilderStore((s) => s.activeFieldId);
|
|
1957
2399
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
1958
2400
|
const activeTab = activeFieldId ? "field" : tab;
|
|
1959
|
-
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(
|
|
1960
2402
|
"button",
|
|
1961
2403
|
{
|
|
1962
2404
|
key: t,
|
|
@@ -1965,26 +2407,29 @@ function ConfigPanel() {
|
|
|
1965
2407
|
className: `bb-config__tab${activeTab === t ? " bb-config__tab--active" : ""}`
|
|
1966
2408
|
},
|
|
1967
2409
|
t
|
|
1968
|
-
))), /* @__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")));
|
|
1969
2411
|
}
|
|
1970
2412
|
|
|
1971
2413
|
// src/block-builder/components/sidebar/FieldPalette.tsx
|
|
1972
|
-
import
|
|
2414
|
+
import React15, { useState as useState6 } from "react";
|
|
1973
2415
|
import {
|
|
1974
2416
|
Type as Type2,
|
|
1975
2417
|
AlignLeft as AlignLeft2,
|
|
1976
|
-
AlignJustify,
|
|
2418
|
+
AlignJustify as AlignJustify2,
|
|
1977
2419
|
Hash as Hash2,
|
|
1978
2420
|
Mail as Mail2,
|
|
1979
2421
|
Calendar as Calendar2,
|
|
1980
2422
|
CheckSquare as CheckSquare2,
|
|
1981
2423
|
ChevronDown as ChevronDown3,
|
|
1982
|
-
Circle
|
|
2424
|
+
Circle,
|
|
1983
2425
|
Upload as Upload2,
|
|
1984
2426
|
Link as Link2,
|
|
1985
|
-
List,
|
|
1986
|
-
Folder,
|
|
1987
|
-
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
|
|
1988
2433
|
} from "lucide-react";
|
|
1989
2434
|
|
|
1990
2435
|
// src/block-builder/lib/field-palette.ts
|
|
@@ -1998,11 +2443,17 @@ var FIELD_PALETTE = [
|
|
|
1998
2443
|
{ type: "checkbox", label: "Checkbox", description: "Boolean toggle", icon: "CheckSquare", category: "basic", color: "bg-blue-500/20 text-blue-400 border-blue-500/30" },
|
|
1999
2444
|
// Choice
|
|
2000
2445
|
{ type: "select", label: "Select", description: "Dropdown selection", icon: "ChevronDown", category: "choice", color: "bg-purple-500/20 text-purple-400 border-purple-500/30" },
|
|
2001
|
-
{ type: "radio", label: "Radio", description: "Radio button group", icon: "Circle", category: "choice", color: "bg-purple-500/20 text-purple-400 border-purple-500/30" },
|
|
2002
2446
|
// Media
|
|
2003
|
-
{ 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" },
|
|
2004
2449
|
// Relational
|
|
2005
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" },
|
|
2006
2457
|
// Advanced
|
|
2007
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" }
|
|
2008
2459
|
];
|
|
@@ -2011,6 +2462,7 @@ var FIELD_CATEGORIES = [
|
|
|
2011
2462
|
{ id: "choice", label: "Choice" },
|
|
2012
2463
|
{ id: "media", label: "Media" },
|
|
2013
2464
|
{ id: "relational", label: "Relational" },
|
|
2465
|
+
{ id: "layout", label: "Layout" },
|
|
2014
2466
|
{ id: "advanced", label: "Advanced" }
|
|
2015
2467
|
];
|
|
2016
2468
|
function getFieldMeta(type) {
|
|
@@ -2021,18 +2473,21 @@ function getFieldMeta(type) {
|
|
|
2021
2473
|
var ICON_MAP2 = {
|
|
2022
2474
|
Type: Type2,
|
|
2023
2475
|
AlignLeft: AlignLeft2,
|
|
2024
|
-
AlignJustify,
|
|
2476
|
+
AlignJustify: AlignJustify2,
|
|
2025
2477
|
Hash: Hash2,
|
|
2026
2478
|
Mail: Mail2,
|
|
2027
2479
|
Calendar: Calendar2,
|
|
2028
2480
|
CheckSquare: CheckSquare2,
|
|
2029
2481
|
ChevronDown: ChevronDown3,
|
|
2030
|
-
Circle
|
|
2482
|
+
Circle,
|
|
2031
2483
|
Upload: Upload2,
|
|
2032
2484
|
Link: Link2,
|
|
2033
|
-
List,
|
|
2034
|
-
Folder,
|
|
2035
|
-
Braces: Braces2
|
|
2485
|
+
List: List2,
|
|
2486
|
+
Folder: Folder2,
|
|
2487
|
+
Braces: Braces2,
|
|
2488
|
+
Image: Image2,
|
|
2489
|
+
Box: Box2,
|
|
2490
|
+
Columns: Columns2
|
|
2036
2491
|
};
|
|
2037
2492
|
function FieldPalette() {
|
|
2038
2493
|
const [search, setSearch] = useState6("");
|
|
@@ -2046,7 +2501,7 @@ function FieldPalette() {
|
|
|
2046
2501
|
if (!activeBlockId || isReadOnly) return;
|
|
2047
2502
|
addField(activeBlockId, type);
|
|
2048
2503
|
}
|
|
2049
|
-
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(
|
|
2050
2505
|
"input",
|
|
2051
2506
|
{
|
|
2052
2507
|
type: "text",
|
|
@@ -2055,7 +2510,7 @@ function FieldPalette() {
|
|
|
2055
2510
|
placeholder: "Search...",
|
|
2056
2511
|
className: "bb-palette__search"
|
|
2057
2512
|
}
|
|
2058
|
-
)), /* @__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(
|
|
2059
2514
|
FieldButton,
|
|
2060
2515
|
{
|
|
2061
2516
|
key: item.type,
|
|
@@ -2067,7 +2522,7 @@ function FieldPalette() {
|
|
|
2067
2522
|
}
|
|
2068
2523
|
))) : FIELD_CATEGORIES.map((cat) => {
|
|
2069
2524
|
const items = FIELD_PALETTE.filter((f) => f.category === cat.id);
|
|
2070
|
-
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(
|
|
2071
2526
|
FieldButton,
|
|
2072
2527
|
{
|
|
2073
2528
|
key: item.type,
|
|
@@ -2078,7 +2533,7 @@ function FieldPalette() {
|
|
|
2078
2533
|
disabled: !activeBlockId || isReadOnly
|
|
2079
2534
|
}
|
|
2080
2535
|
))));
|
|
2081
|
-
})), !activeBlockId && /* @__PURE__ */
|
|
2536
|
+
})), !activeBlockId && /* @__PURE__ */ React15.createElement("div", { className: "bb-sidebar__footer" }, "Select a block first"));
|
|
2082
2537
|
}
|
|
2083
2538
|
function FieldButton({
|
|
2084
2539
|
type,
|
|
@@ -2089,7 +2544,7 @@ function FieldButton({
|
|
|
2089
2544
|
}) {
|
|
2090
2545
|
const meta = getFieldMeta(type);
|
|
2091
2546
|
const Icon = ICON_MAP2[icon];
|
|
2092
|
-
return /* @__PURE__ */
|
|
2547
|
+
return /* @__PURE__ */ React15.createElement(
|
|
2093
2548
|
"button",
|
|
2094
2549
|
{
|
|
2095
2550
|
type: "button",
|
|
@@ -2098,14 +2553,291 @@ function FieldButton({
|
|
|
2098
2553
|
title: meta?.description,
|
|
2099
2554
|
className: "bb-palette__item"
|
|
2100
2555
|
},
|
|
2101
|
-
/* @__PURE__ */
|
|
2102
|
-
/* @__PURE__ */
|
|
2103
|
-
/* @__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)
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
// src/block-builder/components/canvas/CodePreview.tsx
|
|
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)]
|
|
2104
2823
|
);
|
|
2105
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
|
+
}
|
|
2106
2839
|
|
|
2107
2840
|
// src/block-builder/components/canvas/CodePreview.tsx
|
|
2108
|
-
import React15, { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState7 } from "react";
|
|
2109
2841
|
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
2110
2842
|
"import",
|
|
2111
2843
|
"export",
|
|
@@ -2193,22 +2925,22 @@ var TOKEN_COLORS = {
|
|
|
2193
2925
|
};
|
|
2194
2926
|
function HighlightedCode({ code }) {
|
|
2195
2927
|
const tokens = tokenise(code);
|
|
2196
|
-
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)));
|
|
2197
2929
|
}
|
|
2198
2930
|
function CodePreview() {
|
|
2199
2931
|
const blocks = useBuilderStore((s) => s.blocks);
|
|
2200
2932
|
const [fileMap, setFileMap] = useState7({});
|
|
2201
2933
|
const [activeFile, setActiveFile] = useState7(null);
|
|
2202
2934
|
const [copied, setCopied] = useState7(false);
|
|
2203
|
-
const timerRef =
|
|
2204
|
-
const copyTimerRef =
|
|
2935
|
+
const timerRef = useRef5(null);
|
|
2936
|
+
const copyTimerRef = useRef5(null);
|
|
2205
2937
|
const regenerate = useCallback4(() => {
|
|
2206
2938
|
if (blocks.length === 0) {
|
|
2207
2939
|
setFileMap({});
|
|
2208
2940
|
setActiveFile(null);
|
|
2209
2941
|
return;
|
|
2210
2942
|
}
|
|
2211
|
-
const blockOutputs = generateAllBlocks(blocks);
|
|
2943
|
+
const blockOutputs = generateAllBlocks(blocks, { react: true });
|
|
2212
2944
|
const indexOutput = generateIndexFile(blocks);
|
|
2213
2945
|
const next = {};
|
|
2214
2946
|
for (const out of blockOutputs) next[out.filename] = out.code;
|
|
@@ -2219,7 +2951,7 @@ function CodePreview() {
|
|
|
2219
2951
|
return prev && keys.includes(prev) ? prev : keys[0] ?? null;
|
|
2220
2952
|
});
|
|
2221
2953
|
}, [blocks]);
|
|
2222
|
-
|
|
2954
|
+
useEffect5(() => {
|
|
2223
2955
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
2224
2956
|
timerRef.current = setTimeout(regenerate, 300);
|
|
2225
2957
|
return () => {
|
|
@@ -2238,7 +2970,7 @@ function CodePreview() {
|
|
|
2238
2970
|
const activeCode = activeFile ? fileMap[activeFile] ?? "" : "";
|
|
2239
2971
|
const lineCount = activeCode ? activeCode.split("\n").length : 0;
|
|
2240
2972
|
if (fileNames.length === 0) {
|
|
2241
|
-
return /* @__PURE__ */
|
|
2973
|
+
return /* @__PURE__ */ React16.createElement(
|
|
2242
2974
|
"div",
|
|
2243
2975
|
{
|
|
2244
2976
|
className: "code-preview",
|
|
@@ -2255,7 +2987,7 @@ function CodePreview() {
|
|
|
2255
2987
|
"Add a block to see generated TypeScript code"
|
|
2256
2988
|
);
|
|
2257
2989
|
}
|
|
2258
|
-
return /* @__PURE__ */
|
|
2990
|
+
return /* @__PURE__ */ React16.createElement("div", { className: "code-preview", style: { height: "100%", display: "flex", flexDirection: "column", overflow: "hidden" } }, /* @__PURE__ */ React16.createElement(
|
|
2259
2991
|
"div",
|
|
2260
2992
|
{
|
|
2261
2993
|
style: {
|
|
@@ -2269,7 +3001,7 @@ function CodePreview() {
|
|
|
2269
3001
|
},
|
|
2270
3002
|
fileNames.map((name) => {
|
|
2271
3003
|
const isActive = activeFile === name;
|
|
2272
|
-
return /* @__PURE__ */
|
|
3004
|
+
return /* @__PURE__ */ React16.createElement(
|
|
2273
3005
|
"button",
|
|
2274
3006
|
{
|
|
2275
3007
|
key: name,
|
|
@@ -2292,8 +3024,8 @@ function CodePreview() {
|
|
|
2292
3024
|
name
|
|
2293
3025
|
);
|
|
2294
3026
|
}),
|
|
2295
|
-
/* @__PURE__ */
|
|
2296
|
-
lineCount > 0 && /* @__PURE__ */
|
|
3027
|
+
/* @__PURE__ */ React16.createElement("div", { style: { flex: 1 } }),
|
|
3028
|
+
lineCount > 0 && /* @__PURE__ */ React16.createElement(
|
|
2297
3029
|
"span",
|
|
2298
3030
|
{
|
|
2299
3031
|
style: {
|
|
@@ -2306,7 +3038,7 @@ function CodePreview() {
|
|
|
2306
3038
|
lineCount,
|
|
2307
3039
|
" lines"
|
|
2308
3040
|
),
|
|
2309
|
-
/* @__PURE__ */
|
|
3041
|
+
/* @__PURE__ */ React16.createElement(
|
|
2310
3042
|
"button",
|
|
2311
3043
|
{
|
|
2312
3044
|
type: "button",
|
|
@@ -2329,7 +3061,7 @@ function CodePreview() {
|
|
|
2329
3061
|
},
|
|
2330
3062
|
copied ? "Copied" : "Copy"
|
|
2331
3063
|
)
|
|
2332
|
-
), /* @__PURE__ */
|
|
3064
|
+
), /* @__PURE__ */ React16.createElement("div", { style: { flex: 1, overflow: "auto", display: "flex" } }, /* @__PURE__ */ React16.createElement(
|
|
2333
3065
|
"div",
|
|
2334
3066
|
{
|
|
2335
3067
|
style: {
|
|
@@ -2346,8 +3078,8 @@ function CodePreview() {
|
|
|
2346
3078
|
},
|
|
2347
3079
|
"aria-hidden": "true"
|
|
2348
3080
|
},
|
|
2349
|
-
activeCode.split("\n").map((_, i) => /* @__PURE__ */
|
|
2350
|
-
), /* @__PURE__ */
|
|
3081
|
+
activeCode.split("\n").map((_, i) => /* @__PURE__ */ React16.createElement("div", { key: i }, i + 1))
|
|
3082
|
+
), /* @__PURE__ */ React16.createElement(
|
|
2351
3083
|
"pre",
|
|
2352
3084
|
{
|
|
2353
3085
|
style: {
|
|
@@ -2362,37 +3094,198 @@ function CodePreview() {
|
|
|
2362
3094
|
whiteSpace: "pre"
|
|
2363
3095
|
}
|
|
2364
3096
|
},
|
|
2365
|
-
/* @__PURE__ */
|
|
3097
|
+
/* @__PURE__ */ React16.createElement(HighlightedCode, { code: activeCode })
|
|
2366
3098
|
)));
|
|
2367
3099
|
}
|
|
2368
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
|
+
|
|
2369
3248
|
// src/block-builder/components/canvas/BuilderShell.tsx
|
|
2370
3249
|
function BuilderShell({ loadSlug }) {
|
|
2371
3250
|
const loadBlock = useBuilderStore((s) => s.loadBlock);
|
|
2372
3251
|
const setVersionMeta = useBuilderStore((s) => s.setVersionMeta);
|
|
2373
3252
|
const setBlockSlug = useBuilderStore((s) => s.setBlockSlug);
|
|
2374
3253
|
const isReadOnly = useBuilderStore((s) => s.isReadOnly);
|
|
2375
|
-
const [activeSlug, setActiveSlug] =
|
|
2376
|
-
const [loading, setLoading] =
|
|
2377
|
-
const [
|
|
2378
|
-
const [showCodePreview, setShowCodePreview] =
|
|
2379
|
-
const [versions, setVersions] =
|
|
2380
|
-
const [selectedVersionId, setSelectedVersionId] =
|
|
2381
|
-
const [blockDefs, setBlockDefs] =
|
|
2382
|
-
const [mobilePanelTab, setMobilePanelTab] =
|
|
2383
|
-
|
|
2384
|
-
|
|
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);
|
|
3264
|
+
useEffect6(() => {
|
|
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();
|
|
2385
3271
|
setBlockDefs(
|
|
2386
3272
|
(json.docs ?? []).map((d) => ({ id: String(d.id), slug: d.slug, name: d.name }))
|
|
2387
3273
|
);
|
|
2388
|
-
}
|
|
3274
|
+
} catch (err) {
|
|
2389
3275
|
console.error("[block-builder] Failed to load block definitions:", err);
|
|
2390
|
-
|
|
2391
|
-
|
|
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
|
+
}
|
|
2392
3280
|
}, []);
|
|
3281
|
+
useEffect6(() => {
|
|
3282
|
+
refreshBlockDefs(true);
|
|
3283
|
+
}, [refreshBlockDefs]);
|
|
2393
3284
|
const loadVersionsForSlug = useCallback5(async (slug) => {
|
|
2394
3285
|
try {
|
|
2395
|
-
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
|
+
});
|
|
2396
3289
|
const json = await res.json();
|
|
2397
3290
|
return json.versions ?? [];
|
|
2398
3291
|
} catch {
|
|
@@ -2401,20 +3294,22 @@ function BuilderShell({ loadSlug }) {
|
|
|
2401
3294
|
}, []);
|
|
2402
3295
|
const loadVersion = useCallback5(async (slug, versionId) => {
|
|
2403
3296
|
setLoading(true);
|
|
2404
|
-
|
|
3297
|
+
setNotification(null);
|
|
2405
3298
|
const url = versionId ? `/api/block-builder/load/${encodeURIComponent(slug)}?versionId=${encodeURIComponent(versionId)}` : `/api/block-builder/load/${encodeURIComponent(slug)}`;
|
|
2406
3299
|
try {
|
|
2407
|
-
const res = await fetch(url
|
|
3300
|
+
const res = await fetch(url, {
|
|
3301
|
+
headers: { "X-Block-Builder": "1" }
|
|
3302
|
+
});
|
|
2408
3303
|
const json = await res.json();
|
|
2409
3304
|
if (json.block) {
|
|
2410
3305
|
loadBlock(json.block);
|
|
2411
3306
|
setVersionMeta(json.versionId ?? null, !(json.isCurrent ?? true));
|
|
2412
3307
|
setSelectedVersionId(json.versionId ?? null);
|
|
2413
3308
|
} else {
|
|
2414
|
-
|
|
3309
|
+
setNotification({ status: "error", title: "Failed to load block", errors: [json.error ?? "Unknown error"] });
|
|
2415
3310
|
}
|
|
2416
3311
|
} catch (err) {
|
|
2417
|
-
|
|
3312
|
+
setNotification({ status: "error", title: "Network error", errors: [err instanceof Error ? err.message : "Could not reach the server."] });
|
|
2418
3313
|
} finally {
|
|
2419
3314
|
setLoading(false);
|
|
2420
3315
|
}
|
|
@@ -2424,7 +3319,7 @@ function BuilderShell({ loadSlug }) {
|
|
|
2424
3319
|
setBlockSlug(slug);
|
|
2425
3320
|
setVersions([]);
|
|
2426
3321
|
setSelectedVersionId(null);
|
|
2427
|
-
|
|
3322
|
+
setNotification(null);
|
|
2428
3323
|
setMobilePanelTab("canvas");
|
|
2429
3324
|
await loadVersion(slug);
|
|
2430
3325
|
const list = await loadVersionsForSlug(slug);
|
|
@@ -2432,7 +3327,7 @@ function BuilderShell({ loadSlug }) {
|
|
|
2432
3327
|
const current = list.find((v) => v.isCurrent) ?? list[0];
|
|
2433
3328
|
if (current) setSelectedVersionId(current.id);
|
|
2434
3329
|
}, [loadVersion, loadVersionsForSlug, setBlockSlug]);
|
|
2435
|
-
|
|
3330
|
+
useEffect6(() => {
|
|
2436
3331
|
if (!loadSlug) return;
|
|
2437
3332
|
loadBlockBySlug(loadSlug);
|
|
2438
3333
|
}, [loadSlug]);
|
|
@@ -2448,14 +3343,22 @@ function BuilderShell({ loadSlug }) {
|
|
|
2448
3343
|
const current = list.find((v) => v.isCurrent) ?? list[0];
|
|
2449
3344
|
if (current) setSelectedVersionId(current.id);
|
|
2450
3345
|
}
|
|
2451
|
-
async function handleAfterPublish() {
|
|
2452
|
-
|
|
2453
|
-
|
|
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
|
+
]);
|
|
2454
3354
|
setVersions(list);
|
|
2455
3355
|
const current = list.find((v) => v.isCurrent) ?? list[0];
|
|
2456
|
-
|
|
3356
|
+
setSelectedVersionId(current ? current.id : null);
|
|
2457
3357
|
}
|
|
2458
|
-
|
|
3358
|
+
if (!isMounted) {
|
|
3359
|
+
return /* @__PURE__ */ React18.createElement("div", { className: "bb-shell" }, /* @__PURE__ */ React18.createElement("div", { className: "bb-loading-bar" }, "Initializing Builder..."));
|
|
3360
|
+
}
|
|
3361
|
+
return /* @__PURE__ */ React18.createElement(ErrorBoundary, null, /* @__PURE__ */ React18.createElement("div", { className: "bb-shell" }, /* @__PURE__ */ React18.createElement(
|
|
2459
3362
|
TopBar,
|
|
2460
3363
|
{
|
|
2461
3364
|
blockDefs,
|
|
@@ -2465,9 +3368,13 @@ function BuilderShell({ loadSlug }) {
|
|
|
2465
3368
|
selectedVersionId,
|
|
2466
3369
|
onVersionSelect: handleVersionSelect,
|
|
2467
3370
|
onRestoreVersion: handleRestoreVersion,
|
|
2468
|
-
onAfterPublish: handleAfterPublish
|
|
3371
|
+
onAfterPublish: handleAfterPublish,
|
|
3372
|
+
notification,
|
|
3373
|
+
onSetNotification: setNotification,
|
|
3374
|
+
previewOpen,
|
|
3375
|
+
onTogglePreview: () => setPreviewOpen((p) => !p)
|
|
2469
3376
|
}
|
|
2470
|
-
), 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(
|
|
2471
3378
|
"button",
|
|
2472
3379
|
{
|
|
2473
3380
|
type: "button",
|
|
@@ -2475,7 +3382,7 @@ function BuilderShell({ loadSlug }) {
|
|
|
2475
3382
|
onClick: handleRestoreVersion
|
|
2476
3383
|
},
|
|
2477
3384
|
"Switch to latest"
|
|
2478
|
-
))), /* @__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(
|
|
2479
3386
|
"button",
|
|
2480
3387
|
{
|
|
2481
3388
|
type: "button",
|
|
@@ -2484,12 +3391,12 @@ function BuilderShell({ loadSlug }) {
|
|
|
2484
3391
|
},
|
|
2485
3392
|
showCodePreview ? "v" : ">",
|
|
2486
3393
|
" Code Preview"
|
|
2487
|
-
), 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" }, [
|
|
2488
3395
|
{ id: "blocks", icon: "B", label: "Blocks" },
|
|
2489
3396
|
{ id: "canvas", icon: "[]", label: "Canvas" },
|
|
2490
3397
|
{ id: "palette", icon: "+", label: "Fields" },
|
|
2491
3398
|
{ id: "config", icon: "*", label: "Config" }
|
|
2492
|
-
].map(({ id, icon, label }) => /* @__PURE__ */
|
|
3399
|
+
].map(({ id, icon, label }) => /* @__PURE__ */ React18.createElement(
|
|
2493
3400
|
"button",
|
|
2494
3401
|
{
|
|
2495
3402
|
key: id,
|
|
@@ -2497,16 +3404,16 @@ function BuilderShell({ loadSlug }) {
|
|
|
2497
3404
|
className: `bb-mobile-nav__tab${mobilePanelTab === id ? " bb-mobile-nav__tab--active" : ""}`,
|
|
2498
3405
|
onClick: () => setMobilePanelTab(id)
|
|
2499
3406
|
},
|
|
2500
|
-
/* @__PURE__ */
|
|
3407
|
+
/* @__PURE__ */ React18.createElement("span", { className: "bb-mobile-nav__icon" }, icon),
|
|
2501
3408
|
label
|
|
2502
|
-
))));
|
|
3409
|
+
)))));
|
|
2503
3410
|
}
|
|
2504
3411
|
|
|
2505
3412
|
// src/components/BlockBuilderNavLink/index.tsx
|
|
2506
|
-
import
|
|
3413
|
+
import React19 from "react";
|
|
2507
3414
|
import Link3 from "next/link";
|
|
2508
3415
|
function BlockBuilderNavLink() {
|
|
2509
|
-
return /* @__PURE__ */
|
|
3416
|
+
return /* @__PURE__ */ React19.createElement("div", { style: { padding: "0 16px", marginTop: "8px" } }, /* @__PURE__ */ React19.createElement(
|
|
2510
3417
|
Link3,
|
|
2511
3418
|
{
|
|
2512
3419
|
href: "/block-builder",
|
|
@@ -2532,6 +3439,7 @@ function BlockBuilderNavLink() {
|
|
|
2532
3439
|
export {
|
|
2533
3440
|
BlockBuilderNavLink,
|
|
2534
3441
|
BlockDataField,
|
|
3442
|
+
BlockVersionSync,
|
|
2535
3443
|
BuilderShell,
|
|
2536
3444
|
EditInBuilderButton,
|
|
2537
3445
|
SchemaBuilderField
|