@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.
@@ -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