@bettercms-ai/next 0.10.1 → 0.11.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 +11 -1
- package/dist/blocks.js +155 -35
- package/dist/blocks.js.map +1 -1
- package/dist/draft-bridge.d.ts +41 -0
- package/dist/draft-bridge.js +664 -0
- package/dist/draft-bridge.js.map +1 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/visual-editing.d.ts +4 -2
- package/dist/visual-editing.js +1 -1
- package/dist/visual-editing.js.map +1 -1
- package/package.json +9 -4
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/draft-bridge.tsx
|
|
5
|
+
import { useEffect as useEffect2, useState as useState3 } from "react";
|
|
6
|
+
|
|
7
|
+
// src/blocks.tsx
|
|
8
|
+
import {
|
|
9
|
+
cloneElement,
|
|
10
|
+
isValidElement,
|
|
11
|
+
useState as useState2
|
|
12
|
+
} from "react";
|
|
13
|
+
import { blockStyleToCss } from "@bettercms-ai/types";
|
|
14
|
+
import { componentInstanceAddresses } from "@bettercms-ai/types";
|
|
15
|
+
import { applyOverrides } from "@bettercms-ai/ui/resolve";
|
|
16
|
+
|
|
17
|
+
// src/form.tsx
|
|
18
|
+
import { useEffect, useMemo, useState } from "react";
|
|
19
|
+
import {
|
|
20
|
+
submitForm,
|
|
21
|
+
shouldShowField,
|
|
22
|
+
formInitialValues,
|
|
23
|
+
asStringArray,
|
|
24
|
+
toggleOption
|
|
25
|
+
} from "@bettercms-ai/sdk";
|
|
26
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
27
|
+
var TURNSTILE_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
|
28
|
+
var INPUT_TYPES = {
|
|
29
|
+
text: "text",
|
|
30
|
+
email: "email",
|
|
31
|
+
phone: "tel",
|
|
32
|
+
url: "url",
|
|
33
|
+
number: "number",
|
|
34
|
+
date: "date"
|
|
35
|
+
};
|
|
36
|
+
function readPrefill() {
|
|
37
|
+
if (typeof window === "undefined") return {};
|
|
38
|
+
const out = {};
|
|
39
|
+
new URLSearchParams(window.location.search).forEach((v, k) => {
|
|
40
|
+
out[k] = v;
|
|
41
|
+
});
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function resetTurnstile() {
|
|
45
|
+
const t = globalThis.turnstile;
|
|
46
|
+
if (t) {
|
|
47
|
+
try {
|
|
48
|
+
t.reset();
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function BcmsForm({
|
|
54
|
+
form,
|
|
55
|
+
apiBase,
|
|
56
|
+
turnstileSiteKey,
|
|
57
|
+
onSubmitted,
|
|
58
|
+
className
|
|
59
|
+
}) {
|
|
60
|
+
const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
61
|
+
const [values, setValues] = useState(() => {
|
|
62
|
+
const init = formInitialValues(form, readPrefill());
|
|
63
|
+
if (form.honeypotField) init[form.honeypotField] = "";
|
|
64
|
+
return init;
|
|
65
|
+
});
|
|
66
|
+
const [status, setStatus] = useState({ state: "idle" });
|
|
67
|
+
const [fieldErrors, setFieldErrors] = useState({});
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!showTurnstile || document.querySelector(`script[src="${TURNSTILE_SRC}"]`)) return;
|
|
70
|
+
const script = document.createElement("script");
|
|
71
|
+
script.src = TURNSTILE_SRC;
|
|
72
|
+
script.async = true;
|
|
73
|
+
script.defer = true;
|
|
74
|
+
document.head.appendChild(script);
|
|
75
|
+
}, [showTurnstile]);
|
|
76
|
+
const visibleFields = useMemo(
|
|
77
|
+
() => form.fields.filter((f) => f.type !== "hidden" && shouldShowField(f, values)),
|
|
78
|
+
[form.fields, values]
|
|
79
|
+
);
|
|
80
|
+
const setValue = (key, value) => setValues((prev) => ({ ...prev, [key]: value }));
|
|
81
|
+
async function handleSubmit(e) {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
setStatus({ state: "submitting" });
|
|
84
|
+
setFieldErrors({});
|
|
85
|
+
const tokenEl = e.currentTarget.querySelector(
|
|
86
|
+
'[name="cf-turnstile-response"]'
|
|
87
|
+
);
|
|
88
|
+
const turnstileToken = tokenEl?.value || void 0;
|
|
89
|
+
const data = {};
|
|
90
|
+
for (const field of form.fields) {
|
|
91
|
+
if (field.type === "hidden" || shouldShowField(field, values)) {
|
|
92
|
+
data[field.key] = values[field.key];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (form.honeypotField) data[form.honeypotField] = values[form.honeypotField] ?? "";
|
|
96
|
+
try {
|
|
97
|
+
const { id } = await submitForm({ formId: form.id, data, turnstileToken, baseUrl: apiBase });
|
|
98
|
+
onSubmitted?.(id);
|
|
99
|
+
if (form.redirectUrl) {
|
|
100
|
+
window.location.href = form.redirectUrl;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const reset = formInitialValues(form);
|
|
104
|
+
if (form.honeypotField) reset[form.honeypotField] = "";
|
|
105
|
+
setValues(reset);
|
|
106
|
+
setStatus({
|
|
107
|
+
state: "success",
|
|
108
|
+
message: form.successMessage ?? "Thanks! Your submission was received."
|
|
109
|
+
});
|
|
110
|
+
resetTurnstile();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const e2 = err;
|
|
113
|
+
if (e2.fieldErrors) setFieldErrors(e2.fieldErrors);
|
|
114
|
+
setStatus({ state: "error", message: e2.message || "Something went wrong. Please try again." });
|
|
115
|
+
resetTurnstile();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return (
|
|
119
|
+
// `data-bcms-form` carries the id so the Visual Editor can find THIS form in the
|
|
120
|
+
// preview frame and patch its copy live. Valued rather than bare: the runtime
|
|
121
|
+
// selectors are `form[data-bcms-form]`, which a valued attribute still matches.
|
|
122
|
+
/* @__PURE__ */ jsxs("form", { className, onSubmit: handleSubmit, noValidate: true, "data-bcms-form": form.id, children: [
|
|
123
|
+
visibleFields.map((field) => /* @__PURE__ */ jsx(
|
|
124
|
+
Field,
|
|
125
|
+
{
|
|
126
|
+
field,
|
|
127
|
+
value: values[field.key],
|
|
128
|
+
error: fieldErrors[field.key],
|
|
129
|
+
onChange: (v) => setValue(field.key, v)
|
|
130
|
+
},
|
|
131
|
+
field.key
|
|
132
|
+
)),
|
|
133
|
+
form.honeypotField ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { position: "absolute", left: "-9999px" }, children: /* @__PURE__ */ jsx(
|
|
134
|
+
"input",
|
|
135
|
+
{
|
|
136
|
+
type: "text",
|
|
137
|
+
name: form.honeypotField,
|
|
138
|
+
tabIndex: -1,
|
|
139
|
+
autoComplete: "off",
|
|
140
|
+
value: String(values[form.honeypotField] ?? ""),
|
|
141
|
+
onChange: (e) => setValue(form.honeypotField, e.target.value)
|
|
142
|
+
}
|
|
143
|
+
) }) : null,
|
|
144
|
+
showTurnstile ? /* @__PURE__ */ jsx("div", { className: "cf-turnstile", "data-sitekey": turnstileSiteKey }) : null,
|
|
145
|
+
/* @__PURE__ */ jsx("button", { type: "submit", disabled: status.state === "submitting", "data-bcms-form-part": "submit", children: status.state === "submitting" ? "Submitting\u2026" : form.submitLabel ?? "Submit" }),
|
|
146
|
+
status.message ? /* @__PURE__ */ jsx(
|
|
147
|
+
"p",
|
|
148
|
+
{
|
|
149
|
+
className: `bcms-form-status bcms-form-status--${status.state}`,
|
|
150
|
+
role: "status",
|
|
151
|
+
"aria-live": "polite",
|
|
152
|
+
children: status.message
|
|
153
|
+
}
|
|
154
|
+
) : null
|
|
155
|
+
] })
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
function Field({ field, value, error, onChange }) {
|
|
159
|
+
const required = field.required;
|
|
160
|
+
const errorNode = error ? /* @__PURE__ */ jsx("span", { className: "bcms-field-error", role: "alert", children: error }) : null;
|
|
161
|
+
const helpNode = field.helpText ? /* @__PURE__ */ jsx("span", { className: "bcms-field-help", "data-bcms-form-part": "help", children: field.helpText }) : null;
|
|
162
|
+
if (field.type === "checkbox" || field.type === "consent") {
|
|
163
|
+
return /* @__PURE__ */ jsxs(
|
|
164
|
+
"label",
|
|
165
|
+
{
|
|
166
|
+
className: "bcms-field bcms-field--checkbox",
|
|
167
|
+
"data-field": field.key,
|
|
168
|
+
"data-bcms-form-field": field.key,
|
|
169
|
+
children: [
|
|
170
|
+
/* @__PURE__ */ jsx(
|
|
171
|
+
"input",
|
|
172
|
+
{
|
|
173
|
+
type: "checkbox",
|
|
174
|
+
name: field.key,
|
|
175
|
+
required,
|
|
176
|
+
checked: Boolean(value),
|
|
177
|
+
onChange: (e) => onChange(e.target.checked)
|
|
178
|
+
}
|
|
179
|
+
),
|
|
180
|
+
" ",
|
|
181
|
+
/* @__PURE__ */ jsxs("span", { "data-bcms-form-part": "label", children: [
|
|
182
|
+
field.label,
|
|
183
|
+
required ? " *" : ""
|
|
184
|
+
] }),
|
|
185
|
+
helpNode,
|
|
186
|
+
errorNode
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (field.type === "radio" || field.type === "checkboxes") {
|
|
192
|
+
const multi = field.type === "checkboxes";
|
|
193
|
+
const selected = asStringArray(value);
|
|
194
|
+
return /* @__PURE__ */ jsxs(
|
|
195
|
+
"fieldset",
|
|
196
|
+
{
|
|
197
|
+
className: `bcms-field bcms-field--${multi ? "checkboxes" : "radio"}`,
|
|
198
|
+
"data-field": field.key,
|
|
199
|
+
"data-bcms-form-field": field.key,
|
|
200
|
+
children: [
|
|
201
|
+
/* @__PURE__ */ jsxs("legend", { "data-bcms-form-part": "label", children: [
|
|
202
|
+
field.label,
|
|
203
|
+
required ? " *" : ""
|
|
204
|
+
] }),
|
|
205
|
+
(field.options ?? []).map((opt) => /* @__PURE__ */ jsxs("label", { className: "bcms-option", children: [
|
|
206
|
+
/* @__PURE__ */ jsx(
|
|
207
|
+
"input",
|
|
208
|
+
{
|
|
209
|
+
type: multi ? "checkbox" : "radio",
|
|
210
|
+
name: field.key,
|
|
211
|
+
value: opt,
|
|
212
|
+
required: !multi && required,
|
|
213
|
+
checked: multi ? selected.includes(opt) : value === opt,
|
|
214
|
+
onChange: (e) => onChange(multi ? toggleOption(value, opt, e.target.checked) : opt)
|
|
215
|
+
}
|
|
216
|
+
),
|
|
217
|
+
" ",
|
|
218
|
+
/* @__PURE__ */ jsx("span", { "data-bcms-form-part": "option", children: opt })
|
|
219
|
+
] }, opt)),
|
|
220
|
+
helpNode,
|
|
221
|
+
errorNode
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const str = String(value ?? "");
|
|
227
|
+
let control;
|
|
228
|
+
if (field.type === "textarea") {
|
|
229
|
+
control = /* @__PURE__ */ jsx(
|
|
230
|
+
"textarea",
|
|
231
|
+
{
|
|
232
|
+
name: field.key,
|
|
233
|
+
placeholder: field.placeholder,
|
|
234
|
+
required,
|
|
235
|
+
value: str,
|
|
236
|
+
onChange: (e) => onChange(e.target.value)
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
} else if (field.type === "select") {
|
|
240
|
+
control = /* @__PURE__ */ jsxs("select", { name: field.key, required, value: str, onChange: (e) => onChange(e.target.value), children: [
|
|
241
|
+
/* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Choose\u2026" }),
|
|
242
|
+
(field.options ?? []).map((opt) => /* @__PURE__ */ jsx("option", { value: opt, "data-bcms-form-part": "option", children: opt }, opt))
|
|
243
|
+
] });
|
|
244
|
+
} else {
|
|
245
|
+
control = /* @__PURE__ */ jsx(
|
|
246
|
+
"input",
|
|
247
|
+
{
|
|
248
|
+
type: INPUT_TYPES[field.type] ?? "text",
|
|
249
|
+
name: field.key,
|
|
250
|
+
placeholder: field.placeholder,
|
|
251
|
+
required,
|
|
252
|
+
value: str,
|
|
253
|
+
onChange: (e) => onChange(e.target.value)
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return /* @__PURE__ */ jsxs("p", { className: "bcms-field", "data-field": field.key, "data-bcms-form-field": field.key, children: [
|
|
258
|
+
/* @__PURE__ */ jsxs("label", { "data-bcms-form-part": "label", children: [
|
|
259
|
+
field.label,
|
|
260
|
+
required ? " *" : ""
|
|
261
|
+
] }),
|
|
262
|
+
/* @__PURE__ */ jsx("br", {}),
|
|
263
|
+
control,
|
|
264
|
+
helpNode,
|
|
265
|
+
errorNode
|
|
266
|
+
] });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/bindings.ts
|
|
270
|
+
import { formatPropsAttribute } from "@bettercms-ai/types";
|
|
271
|
+
function addressFor(id, prop, ctx) {
|
|
272
|
+
if (!id) return null;
|
|
273
|
+
const key = `${id}__${prop}`;
|
|
274
|
+
if (ctx.fieldAddresses) return ctx.fieldAddresses.get(key) ?? null;
|
|
275
|
+
return key;
|
|
276
|
+
}
|
|
277
|
+
function bind(id, prop, kind, ctx) {
|
|
278
|
+
const address = addressFor(id, prop, ctx);
|
|
279
|
+
return address ? { "data-bcms-field": address, "data-bcms-kind": kind } : {};
|
|
280
|
+
}
|
|
281
|
+
function bindProps(id, entries, ctx) {
|
|
282
|
+
const resolved = entries.flatMap(({ prop, kind, attr, present }) => {
|
|
283
|
+
if (!present) return [];
|
|
284
|
+
const address = addressFor(id, prop, ctx);
|
|
285
|
+
return address ? [{ address, kind, attr }] : [];
|
|
286
|
+
});
|
|
287
|
+
return resolved.length ? { "data-bcms-props": formatPropsAttribute(resolved) } : {};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/blocks.tsx
|
|
291
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
292
|
+
var MAX_COMPONENT_DEPTH = 10;
|
|
293
|
+
function BcmsBlocks({
|
|
294
|
+
blocks,
|
|
295
|
+
forms,
|
|
296
|
+
components,
|
|
297
|
+
turnstileSiteKey,
|
|
298
|
+
apiBase,
|
|
299
|
+
className
|
|
300
|
+
}) {
|
|
301
|
+
const formsById = new Map((forms ?? []).map((f) => [f.id, f]));
|
|
302
|
+
const componentsById = new Map((components ?? []).map((cmp) => [cmp.id, cmp]));
|
|
303
|
+
const ctx = { formsById, componentsById, turnstileSiteKey, apiBase, depth: 0, seen: /* @__PURE__ */ new Set() };
|
|
304
|
+
return (
|
|
305
|
+
/**
|
|
306
|
+
* 🔴 `data-bcms-page` marks THIS SUBTREE as ours to edit — the SDK's equivalent of the hosted
|
|
307
|
+
* renderer's `<main class="bcms-page">` (render-page.ts).
|
|
308
|
+
*
|
|
309
|
+
* The Visual Editor decides whether to show section controls by asking the page what it is.
|
|
310
|
+
* Its only test was that hosted wrapper, which an SDK-rendered site never has — so every
|
|
311
|
+
* headless site was treated as "not ours", and move/duplicate/delete/reorder stayed hidden
|
|
312
|
+
* on pages that fully support them. Measured on a live SDK site: 34 blocks rendered,
|
|
313
|
+
* 0 recognised.
|
|
314
|
+
*
|
|
315
|
+
* It must sit on the CONTAINER rather than be inferred from the blocks, so that a page with
|
|
316
|
+
* NO blocks yet is still recognised — otherwise there is no way to add the first section,
|
|
317
|
+
* which is exactly the state a new page starts in.
|
|
318
|
+
*/
|
|
319
|
+
/* @__PURE__ */ jsx2("div", { className, "data-bcms-page": "", children: blocks.map((block) => tagTopLevel(block, renderBlock(block, ctx))) })
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
function tagTopLevel(block, node) {
|
|
323
|
+
if (!isValidElement(node)) return node;
|
|
324
|
+
return cloneElement(node, {
|
|
325
|
+
"data-bcms-block": block.id
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function renderBlock(block, ctx) {
|
|
329
|
+
const node = renderBlockInner(block, ctx);
|
|
330
|
+
const style = blockStyleToCss(block.style);
|
|
331
|
+
if (!style || node == null) return node;
|
|
332
|
+
return /* @__PURE__ */ jsx2("div", { style, children: node }, block.id);
|
|
333
|
+
}
|
|
334
|
+
function renderBlockInner(block, ctx) {
|
|
335
|
+
switch (block.type) {
|
|
336
|
+
case "heading":
|
|
337
|
+
return renderHeading(block, ctx);
|
|
338
|
+
// `richtext` and `text` carry the same payload and differ only in the authoring surface,
|
|
339
|
+
// so delivery renders them identically — Astro (BcmsBlocks.astro) and the hosted renderer
|
|
340
|
+
// (render-page.ts) both alias them. Next did not, so `richtext` fell to `default: null`
|
|
341
|
+
// and a richtext block rendered NOTHING on a Next site: 200 OK, green build, blank region.
|
|
342
|
+
case "text":
|
|
343
|
+
case "richtext":
|
|
344
|
+
return renderText(block, ctx);
|
|
345
|
+
case "image":
|
|
346
|
+
return renderImage(block, ctx);
|
|
347
|
+
case "button":
|
|
348
|
+
return renderButton(block, ctx);
|
|
349
|
+
case "spacer":
|
|
350
|
+
return renderSpacer(block);
|
|
351
|
+
case "video":
|
|
352
|
+
return renderVideo(block, ctx);
|
|
353
|
+
case "columns":
|
|
354
|
+
return renderColumns(block, ctx);
|
|
355
|
+
case "form":
|
|
356
|
+
return renderForm(block, ctx);
|
|
357
|
+
case "section":
|
|
358
|
+
return renderSection(block, ctx);
|
|
359
|
+
case "navbar":
|
|
360
|
+
return renderNavbar(block, ctx);
|
|
361
|
+
case "footer":
|
|
362
|
+
return renderFooter(block, ctx);
|
|
363
|
+
case "slider":
|
|
364
|
+
return /* @__PURE__ */ jsx2(BcmsSlider, { block, ctx }, block.id);
|
|
365
|
+
case "tabs":
|
|
366
|
+
return /* @__PURE__ */ jsx2(BcmsTabs, { block, ctx }, block.id);
|
|
367
|
+
case "component":
|
|
368
|
+
return renderComponent(block, ctx);
|
|
369
|
+
default:
|
|
370
|
+
return renderUnhandledBlock(block);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function renderHeading({ id, props }, ctx) {
|
|
374
|
+
const Tag = `h${props.level ?? 2}`;
|
|
375
|
+
return /* @__PURE__ */ jsx2(Tag, { className: "bcms-block bcms-block--heading", ...bind(id, "text", "text", ctx), children: props.text }, id);
|
|
376
|
+
}
|
|
377
|
+
function renderText({ id, props }, ctx) {
|
|
378
|
+
return /* @__PURE__ */ jsx2(
|
|
379
|
+
"div",
|
|
380
|
+
{
|
|
381
|
+
className: "bcms-block bcms-block--text",
|
|
382
|
+
...bind(id, "html", "richtext", ctx),
|
|
383
|
+
dangerouslySetInnerHTML: { __html: props.html }
|
|
384
|
+
},
|
|
385
|
+
id
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
function renderImage({ id, props }, ctx) {
|
|
389
|
+
const img = /* @__PURE__ */ jsx2(
|
|
390
|
+
"img",
|
|
391
|
+
{
|
|
392
|
+
src: props.src,
|
|
393
|
+
alt: props.alt,
|
|
394
|
+
width: props.width,
|
|
395
|
+
height: props.height,
|
|
396
|
+
loading: "lazy",
|
|
397
|
+
...bind(id, "src", "image", ctx),
|
|
398
|
+
...bindProps(id, [{ prop: "alt", kind: "text", attr: "alt", present: props.alt != null }], ctx)
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
if (!props.caption) {
|
|
402
|
+
return /* @__PURE__ */ jsx2("figure", { className: "bcms-block bcms-block--image", children: img }, id);
|
|
403
|
+
}
|
|
404
|
+
return /* @__PURE__ */ jsxs2("figure", { className: "bcms-block bcms-block--image", children: [
|
|
405
|
+
img,
|
|
406
|
+
/* @__PURE__ */ jsx2("figcaption", { ...bind(id, "caption", "text", ctx), children: props.caption })
|
|
407
|
+
] }, id);
|
|
408
|
+
}
|
|
409
|
+
function renderButton({ id, props }, ctx) {
|
|
410
|
+
return /* @__PURE__ */ jsx2(
|
|
411
|
+
"a",
|
|
412
|
+
{
|
|
413
|
+
href: props.href,
|
|
414
|
+
className: `bcms-block bcms-block--button bcms-button--${props.variant ?? "primary"}`,
|
|
415
|
+
...bind(id, "text", "text", ctx),
|
|
416
|
+
...bindProps(id, [{ prop: "href", kind: "url", attr: "href", present: props.href != null }], ctx),
|
|
417
|
+
children: props.text
|
|
418
|
+
},
|
|
419
|
+
id
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
function renderSpacer({ id, props }) {
|
|
423
|
+
return /* @__PURE__ */ jsx2(
|
|
424
|
+
"div",
|
|
425
|
+
{
|
|
426
|
+
className: "bcms-block bcms-block--spacer",
|
|
427
|
+
style: { height: props.height },
|
|
428
|
+
"aria-hidden": "true"
|
|
429
|
+
},
|
|
430
|
+
id
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
function renderVideo({ id, props }, ctx) {
|
|
434
|
+
return /* @__PURE__ */ jsx2(
|
|
435
|
+
"video",
|
|
436
|
+
{
|
|
437
|
+
className: "bcms-block bcms-block--video",
|
|
438
|
+
src: props.url,
|
|
439
|
+
poster: props.poster,
|
|
440
|
+
...bindProps(
|
|
441
|
+
id,
|
|
442
|
+
[
|
|
443
|
+
{ prop: "url", kind: "url", attr: "src", present: props.url != null },
|
|
444
|
+
{ prop: "poster", kind: "url", attr: "poster", present: props.poster != null }
|
|
445
|
+
],
|
|
446
|
+
ctx
|
|
447
|
+
),
|
|
448
|
+
autoPlay: props.autoplay,
|
|
449
|
+
loop: props.loop,
|
|
450
|
+
controls: true
|
|
451
|
+
},
|
|
452
|
+
id
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
function renderColumns({ id, props }, ctx) {
|
|
456
|
+
return /* @__PURE__ */ jsx2(
|
|
457
|
+
"div",
|
|
458
|
+
{
|
|
459
|
+
className: "bcms-block bcms-block--columns",
|
|
460
|
+
style: {
|
|
461
|
+
display: "grid",
|
|
462
|
+
gridTemplateColumns: `repeat(${props.columns.length || 1}, 1fr)`,
|
|
463
|
+
gap: props.gap
|
|
464
|
+
},
|
|
465
|
+
children: props.columns.map((column, i) => /* @__PURE__ */ jsx2("div", { className: "bcms-column", children: column.map((child) => renderBlock(child, ctx)) }, i))
|
|
466
|
+
},
|
|
467
|
+
id
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
function renderForm({ id, props }, ctx) {
|
|
471
|
+
const form = ctx.formsById.get(props.formId);
|
|
472
|
+
if (!form) return null;
|
|
473
|
+
return /* @__PURE__ */ jsx2("div", { className: "bcms-block bcms-block--form", children: /* @__PURE__ */ jsx2(BcmsForm, { form, turnstileSiteKey: ctx.turnstileSiteKey, apiBase: ctx.apiBase }) }, id);
|
|
474
|
+
}
|
|
475
|
+
function renderSection({ id, props }, ctx) {
|
|
476
|
+
const inner = props.children.map((child) => renderBlock(child, ctx));
|
|
477
|
+
return /* @__PURE__ */ jsx2(
|
|
478
|
+
"section",
|
|
479
|
+
{
|
|
480
|
+
className: "bcms-block bcms-block--section",
|
|
481
|
+
style: { background: props.background, padding: props.paddingY != null ? `${props.paddingY}px 0` : void 0 },
|
|
482
|
+
children: props.maxWidth ? /* @__PURE__ */ jsx2("div", { className: "bcms-section-inner", style: { maxWidth: props.maxWidth, margin: "0 auto" }, children: inner }) : inner
|
|
483
|
+
},
|
|
484
|
+
id
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
function renderNavbar({ id, props }, ctx) {
|
|
488
|
+
return /* @__PURE__ */ jsxs2("nav", { className: "bcms-block bcms-block--navbar", children: [
|
|
489
|
+
props.logo?.src ? /* @__PURE__ */ jsx2("a", { className: "bcms-navbar-logo", href: props.logo.href ?? "/", children: /* @__PURE__ */ jsx2(
|
|
490
|
+
"img",
|
|
491
|
+
{
|
|
492
|
+
src: props.logo.src,
|
|
493
|
+
alt: props.logo.alt,
|
|
494
|
+
...bind(id, "logo.src", "image", ctx),
|
|
495
|
+
...bindProps(id, [{ prop: "logo.alt", kind: "text", attr: "alt", present: props.logo.alt != null }], ctx)
|
|
496
|
+
}
|
|
497
|
+
) }) : null,
|
|
498
|
+
/* @__PURE__ */ jsx2("div", { className: "bcms-navbar-links", children: props.links.map((l, i) => /* @__PURE__ */ jsx2(
|
|
499
|
+
"a",
|
|
500
|
+
{
|
|
501
|
+
href: l.href,
|
|
502
|
+
...bind(id, `links[${i}].label`, "text", ctx),
|
|
503
|
+
...bindProps(id, [{ prop: `links[${i}].href`, kind: "url", attr: "href", present: l.href != null }], ctx),
|
|
504
|
+
children: l.label
|
|
505
|
+
},
|
|
506
|
+
i
|
|
507
|
+
)) }),
|
|
508
|
+
props.cta ? /* @__PURE__ */ jsx2(
|
|
509
|
+
"a",
|
|
510
|
+
{
|
|
511
|
+
className: "bcms-button bcms-button--primary",
|
|
512
|
+
href: props.cta.href,
|
|
513
|
+
...bind(id, "cta.text", "text", ctx),
|
|
514
|
+
...bindProps(id, [{ prop: "cta.href", kind: "url", attr: "href", present: props.cta.href != null }], ctx),
|
|
515
|
+
children: props.cta.text
|
|
516
|
+
}
|
|
517
|
+
) : null
|
|
518
|
+
] }, id);
|
|
519
|
+
}
|
|
520
|
+
function renderFooter({ id, props }, ctx) {
|
|
521
|
+
return /* @__PURE__ */ jsxs2("footer", { className: "bcms-block bcms-block--footer", children: [
|
|
522
|
+
/* @__PURE__ */ jsx2("div", { className: "bcms-footer-cols", children: props.columns.map((col, i) => /* @__PURE__ */ jsxs2("div", { className: "bcms-footer-col", children: [
|
|
523
|
+
col.heading ? /* @__PURE__ */ jsx2("h4", { ...bind(id, `columns[${i}].heading`, "text", ctx), children: col.heading }) : null,
|
|
524
|
+
/* @__PURE__ */ jsx2("ul", { children: col.links.map((l, j) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsx2(
|
|
525
|
+
"a",
|
|
526
|
+
{
|
|
527
|
+
href: l.href,
|
|
528
|
+
...bind(id, `columns[${i}].links[${j}].label`, "text", ctx),
|
|
529
|
+
...bindProps(
|
|
530
|
+
id,
|
|
531
|
+
[{ prop: `columns[${i}].links[${j}].href`, kind: "url", attr: "href", present: l.href != null }],
|
|
532
|
+
ctx
|
|
533
|
+
),
|
|
534
|
+
children: l.label
|
|
535
|
+
}
|
|
536
|
+
) }, j)) })
|
|
537
|
+
] }, i)) }),
|
|
538
|
+
props.copyright ? /* @__PURE__ */ jsx2("p", { className: "bcms-footer-copy", ...bind(id, "copyright", "text", ctx), children: props.copyright }) : null
|
|
539
|
+
] }, id);
|
|
540
|
+
}
|
|
541
|
+
function BcmsSlider({ block, ctx, ...rest }) {
|
|
542
|
+
const { slides } = block.props;
|
|
543
|
+
const [i, setI] = useState2(0);
|
|
544
|
+
const go = (n) => setI((n + slides.length) % (slides.length || 1));
|
|
545
|
+
return /* @__PURE__ */ jsxs2("div", { className: "bcms-block bcms-block--slider", style: { position: "relative" }, ...rest, children: [
|
|
546
|
+
slides.map((s, idx) => /* @__PURE__ */ jsx2("div", { className: "bcms-slide", hidden: idx !== i, children: s.children.map((child) => renderBlock(child, ctx)) }, s.id)),
|
|
547
|
+
/* @__PURE__ */ jsx2("button", { type: "button", className: "bcms-slider-prev", "aria-label": "Previous", onClick: () => go(i - 1), children: "\u2039" }),
|
|
548
|
+
/* @__PURE__ */ jsx2("button", { type: "button", className: "bcms-slider-next", "aria-label": "Next", onClick: () => go(i + 1), children: "\u203A" })
|
|
549
|
+
] });
|
|
550
|
+
}
|
|
551
|
+
function BcmsTabs({ block, ctx, ...rest }) {
|
|
552
|
+
const { tabs } = block.props;
|
|
553
|
+
const [active, setActive] = useState2(0);
|
|
554
|
+
return /* @__PURE__ */ jsxs2("div", { className: "bcms-block bcms-block--tabs", ...rest, children: [
|
|
555
|
+
/* @__PURE__ */ jsx2("div", { className: "bcms-tablist", role: "tablist", children: tabs.map((t, idx) => /* @__PURE__ */ jsx2(
|
|
556
|
+
"button",
|
|
557
|
+
{
|
|
558
|
+
role: "tab",
|
|
559
|
+
type: "button",
|
|
560
|
+
className: "bcms-tab",
|
|
561
|
+
"aria-selected": idx === active,
|
|
562
|
+
onClick: () => setActive(idx),
|
|
563
|
+
...bind(block.id, `tabs[${idx}].label`, "text", ctx),
|
|
564
|
+
children: t.label
|
|
565
|
+
},
|
|
566
|
+
t.id
|
|
567
|
+
)) }),
|
|
568
|
+
tabs.map((t, idx) => /* @__PURE__ */ jsx2("div", { role: "tabpanel", className: "bcms-tabpanel", hidden: idx !== active, children: t.children.map((child) => renderBlock(child, ctx)) }, t.id))
|
|
569
|
+
] });
|
|
570
|
+
}
|
|
571
|
+
var warnedMissing = /* @__PURE__ */ new Set();
|
|
572
|
+
function renderComponent({ id, props }, ctx) {
|
|
573
|
+
const def = ctx.componentsById.get(props.componentId);
|
|
574
|
+
if (!def) return renderMissingComponent(props.componentId, ctx);
|
|
575
|
+
if (ctx.depth >= MAX_COMPONENT_DEPTH || ctx.seen.has(def.id)) return null;
|
|
576
|
+
const resolved = applyOverrides(def.blockJson, def.props ?? [], props.overrides ?? {});
|
|
577
|
+
const childCtx = {
|
|
578
|
+
...ctx,
|
|
579
|
+
depth: ctx.depth + 1,
|
|
580
|
+
seen: /* @__PURE__ */ new Set([...ctx.seen, def.id]),
|
|
581
|
+
fieldAddresses: componentInstanceAddresses(id, def.props)
|
|
582
|
+
};
|
|
583
|
+
return /* @__PURE__ */ jsx2("div", { className: "bcms-block bcms-block--component", children: resolved.map((child) => renderBlock(child, childCtx)) }, id);
|
|
584
|
+
}
|
|
585
|
+
var warnedUnhandled = /* @__PURE__ */ new Set();
|
|
586
|
+
function renderUnhandledBlock(block) {
|
|
587
|
+
if (!warnedUnhandled.has(block.type)) {
|
|
588
|
+
warnedUnhandled.add(block.type);
|
|
589
|
+
console.warn(
|
|
590
|
+
`[BcmsBlocks] block type "${block.type}" has no renderer here, so it rendered NOTHING. The hosted renderer draws it; this SDK does not yet.`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
function renderMissingComponent(componentId, ctx) {
|
|
596
|
+
if (!warnedMissing.has(componentId)) {
|
|
597
|
+
warnedMissing.add(componentId);
|
|
598
|
+
console.warn(
|
|
599
|
+
`[BcmsBlocks] component "${componentId}" was not delivered, so this section rendered NOTHING.` + (ctx.componentsById.size === 0 ? ` No components were passed at all \u2014 add components={readComponents()} to <BcmsBlocks>.` : ` ${ctx.componentsById.size} component(s) were passed but none match. It is probably still a DRAFT: publish it, then rebuild.`)
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (process.env.NODE_ENV === "production") return null;
|
|
603
|
+
return /* @__PURE__ */ jsxs2(
|
|
604
|
+
"div",
|
|
605
|
+
{
|
|
606
|
+
className: "bcms-block bcms-block--component-missing",
|
|
607
|
+
"data-bcms-missing-component": componentId,
|
|
608
|
+
style: {
|
|
609
|
+
padding: "12px 16px",
|
|
610
|
+
border: "1px dashed #d4d4d8",
|
|
611
|
+
borderRadius: 8,
|
|
612
|
+
color: "#71717a",
|
|
613
|
+
font: "500 13px/1.5 system-ui, -apple-system, sans-serif"
|
|
614
|
+
},
|
|
615
|
+
children: [
|
|
616
|
+
"Section ",
|
|
617
|
+
/* @__PURE__ */ jsx2("code", { children: componentId }),
|
|
618
|
+
" was not delivered.",
|
|
619
|
+
" ",
|
|
620
|
+
ctx.componentsById.size === 0 ? "Pass components={readComponents()} to <BcmsBlocks>." : "Publish the component, then rebuild."
|
|
621
|
+
]
|
|
622
|
+
}
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/draft-bridge.tsx
|
|
627
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
628
|
+
function BcmsDraftBridge({
|
|
629
|
+
slug,
|
|
630
|
+
blocks,
|
|
631
|
+
components,
|
|
632
|
+
...rest
|
|
633
|
+
}) {
|
|
634
|
+
const [doc, setDoc] = useState3({ blocks, components });
|
|
635
|
+
useEffect2(() => {
|
|
636
|
+
if (window.parent === window) return;
|
|
637
|
+
let handshaken = false;
|
|
638
|
+
const onMessage = (e) => {
|
|
639
|
+
if (e.origin !== window.location.origin) return;
|
|
640
|
+
if (e.source !== window.parent) return;
|
|
641
|
+
const d = e.data;
|
|
642
|
+
if (!d || typeof d !== "object") return;
|
|
643
|
+
if (d.type === "bcms:bridge-init") {
|
|
644
|
+
handshaken = true;
|
|
645
|
+
window.parent.postMessage({ type: "bcms:bridge-ready", slug }, e.origin);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (!handshaken) return;
|
|
649
|
+
if (d.type === "bcms:draft-doc" && d.slug === slug && Array.isArray(d.blocks)) {
|
|
650
|
+
setDoc({
|
|
651
|
+
blocks: d.blocks,
|
|
652
|
+
components: Array.isArray(d.components) ? d.components : components
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
window.addEventListener("message", onMessage);
|
|
657
|
+
return () => window.removeEventListener("message", onMessage);
|
|
658
|
+
}, [slug, components]);
|
|
659
|
+
return /* @__PURE__ */ jsx3(BcmsBlocks, { blocks: doc.blocks, components: doc.components, ...rest });
|
|
660
|
+
}
|
|
661
|
+
export {
|
|
662
|
+
BcmsDraftBridge
|
|
663
|
+
};
|
|
664
|
+
//# sourceMappingURL=draft-bridge.js.map
|