@bettercms-ai/ui 0.6.0 → 0.7.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.
@@ -18,7 +18,7 @@ interface TextBlockProps extends TextProps, BcmsBindingProps {
18
18
  className?: string;
19
19
  align?: "left" | "center" | "right";
20
20
  /** Portable Text source. WINS over `html`, which is the derived cache. */
21
- content?: PortableTextDocument;
21
+ content?: PortableTextDocument | unknown[];
22
22
  }
23
23
  declare function Text({ html, content, align, className, "data-bcms-field": field, "data-bcms-kind": kind, }: TextBlockProps): react_jsx_runtime.JSX.Element;
24
24
 
package/dist/blocks.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { a as Button, C as Columns, H as Heading, I as Image, S as Spacer, T as Text, V as Video } from './Video-DRC_fPjV.js';
1
+ export { a as Button, C as Columns, H as Heading, I as Image, S as Spacer, T as Text, V as Video } from './Video-CZkWQ3Hm.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { SectionProps, ContentBlock, NavbarProps, FooterProps, SliderProps, TabsProps, ComponentProps, FormProps } from '@bettercms-ai/types';
4
4
  import React from 'react';
@@ -45,4 +45,11 @@ declare function FormPlaceholder({ formId, className }: FormProps & {
45
45
  className?: string;
46
46
  }): react_jsx_runtime.JSX.Element;
47
47
 
48
- export { ComponentInstance, Footer, FormPlaceholder, Navbar, Section, Slider, Tabs };
48
+ interface PortableTextProps {
49
+ value: unknown;
50
+ /** `<blockId>__<prop>`. Present ⇒ stamp a per-block address. Absent ⇒ published output. */
51
+ bindingBase?: string;
52
+ }
53
+ declare function PortableText({ value, bindingBase }: PortableTextProps): react_jsx_runtime.JSX.Element | null;
54
+
55
+ export { ComponentInstance, Footer, FormPlaceholder, Navbar, PortableText, type PortableTextProps, Section, Slider, Tabs };
package/dist/blocks.js CHANGED
@@ -30,42 +30,420 @@ function Heading({
30
30
 
31
31
  // src/blocks/portableText.tsx
32
32
  import React from "react";
33
- import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
34
- var MARK_TAGS = {
35
- strong: "strong",
36
- em: "em",
37
- underline: "u",
38
- "strike-through": "s",
39
- code: "code"
33
+ import { PortableText as PT } from "@portabletext/react";
34
+
35
+ // ../richtext/src/types.ts
36
+ var PT_TYPES = {
37
+ /** A generic embedded block: `{schemaKey, schemaLabel, fields}`, the successor to Lexical's BlockNode. */
38
+ block: "bcmsBlock",
39
+ /** An inline mention. A PT inline object is natively void, which is what Lexical's `token` mode was faking. */
40
+ mention: "bcmsMention",
41
+ divider: "bcmsDivider",
42
+ code: "bcmsCode",
43
+ /**
44
+ * A table. Spelled `table`, unprefixed, because that is what every other Portable Text
45
+ * producer calls one — the Sanity-shaped `{_type:"table", rows}` this platform used to refuse
46
+ * as foreign content now lands on OUR type and renders, rather than being data nothing draws.
47
+ */
48
+ table: "table",
49
+ /** The annotation carrying a hyperlink's `href`. Spelled `link` because that is the spec's convention. */
50
+ link: "link",
51
+ /**
52
+ * The annotation carrying a site's OWN inline markup — `<span class>`, `<small>`, `<time
53
+ * datetime>`. Unprefixed, like `table`, because it describes HTML rather than a BetterCMS
54
+ * concept. See `styled.ts` for what it may hold and why the list is that short.
55
+ */
56
+ styled: "styled"
40
57
  };
41
- var BLOCK_TAGS = {
42
- normal: "p",
43
- h1: "h1",
44
- h2: "h2",
45
- h3: "h3",
46
- h4: "h4",
47
- h5: "h5",
48
- h6: "h6",
49
- blockquote: "blockquote"
58
+ var BUILTIN_TABLE_KEY = "builtin:table";
59
+ var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
60
+ "block",
61
+ PT_TYPES.block,
62
+ PT_TYPES.table,
63
+ PT_TYPES.code,
64
+ PT_TYPES.divider,
65
+ PT_TYPES.mention
66
+ ]);
67
+
68
+ // ../richtext/src/styled.ts
69
+ var STYLED_TAGS = ["span", "small", "sup", "sub", "kbd", "abbr", "time"];
70
+ var TAG_SET = new Set(STYLED_TAGS);
71
+ var isStyledTag = (tag) => typeof tag === "string" && TAG_SET.has(tag);
72
+ var ATTR_NAMES = /* @__PURE__ */ new Set(["title", "datetime", "style", "aria-label"]);
73
+ var DATA_ATTR = /^data-[a-z][a-z0-9-]*$/;
74
+ var isStyledAttr = (name) => ATTR_NAMES.has(name) || DATA_ATTR.test(name) && !name.startsWith("data-bcms-");
75
+ var STYLE_PROPS = /* @__PURE__ */ new Set([
76
+ "color",
77
+ "background-color",
78
+ "opacity",
79
+ "font",
80
+ "font-family",
81
+ "font-size",
82
+ "font-style",
83
+ "font-weight",
84
+ "font-variant",
85
+ "font-feature-settings",
86
+ "font-variation-settings",
87
+ "letter-spacing",
88
+ "line-height",
89
+ "word-spacing",
90
+ "white-space",
91
+ "word-break",
92
+ "text-align",
93
+ "text-decoration",
94
+ "text-decoration-color",
95
+ "text-decoration-line",
96
+ "text-decoration-style",
97
+ "text-transform",
98
+ "text-underline-offset",
99
+ "vertical-align"
100
+ ]);
101
+ var STYLE_VALUE_BAN = /url\(|expression\(|@import|\\|\/\*|[<>]/i;
102
+ function safeStyle(css) {
103
+ const kept = [];
104
+ for (const declaration of css.split(";")) {
105
+ const at = declaration.indexOf(":");
106
+ if (at < 0) continue;
107
+ const name = declaration.slice(0, at).trim().toLowerCase();
108
+ const value = declaration.slice(at + 1).trim();
109
+ if (!name || !value) continue;
110
+ if (!name.startsWith("--") && !STYLE_PROPS.has(name)) continue;
111
+ if (STYLE_VALUE_BAN.test(value)) continue;
112
+ kept.push(`${name}:${value}`);
113
+ }
114
+ return kept.length ? kept.join(";") : void 0;
115
+ }
116
+ function styledAttrEntries(attrs) {
117
+ if (!attrs || typeof attrs !== "object" || Array.isArray(attrs)) return [];
118
+ const out = [];
119
+ for (const [name, raw] of Object.entries(attrs)) {
120
+ if (!isStyledAttr(name) || typeof raw !== "string") continue;
121
+ const value = name === "style" ? safeStyle(raw) : raw;
122
+ if (value === void 0) continue;
123
+ out.push([name, value]);
124
+ }
125
+ return out;
126
+ }
127
+
128
+ // ../richtext/src/href.ts
129
+ var SAFE_HREF_SCHEMES = ["http:", "https:", "mailto:", "tel:"];
130
+ function safeHref(raw) {
131
+ const trimmed = raw.trim();
132
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) return trimmed;
133
+ try {
134
+ return SAFE_HREF_SCHEMES.includes(new URL(trimmed).protocol) ? trimmed : null;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
140
+ // ../richtext/src/key.ts
141
+ var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
142
+ var KEY_PATTERN = /^[A-Za-z0-9-]{1,32}$/;
143
+ var isValidKey = (k) => typeof k === "string" && KEY_PATTERN.test(k);
144
+ function randomKey(length = 12) {
145
+ let out = "";
146
+ for (let i = 0; i < length; i += 1) {
147
+ out += ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
148
+ }
149
+ return out;
150
+ }
151
+ function deterministicKeys(prefix = "k") {
152
+ let n = 0;
153
+ return () => {
154
+ n += 1;
155
+ return `${prefix}${n}`;
156
+ };
157
+ }
158
+
159
+ // ../richtext/src/table.ts
160
+ var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
161
+ var isPortableTextTable = (n) => isRecord(n) && n._type === PT_TYPES.table && Array.isArray(n.rows);
162
+ var isBuiltInTableBlock = (n) => isRecord(n) && n._type === PT_TYPES.block && n.schemaKey === BUILTIN_TABLE_KEY;
163
+ var asIs = (v) => Array.isArray(v) ? v : [];
164
+ function keyMinter(base, reserved) {
165
+ const prefix = (base.match(/[A-Za-z0-9-]/g) ?? []).join("").slice(0, 16) || "t";
166
+ return (...parts) => {
167
+ const candidate = `${prefix}-${parts.join("")}`.slice(0, 30);
168
+ let key = candidate;
169
+ for (let n = 1; reserved.has(key); n += 1) key = `${candidate}-${n}`;
170
+ reserved.add(key);
171
+ return key;
172
+ };
173
+ }
174
+ function reservedKeys(rows) {
175
+ const out = /* @__PURE__ */ new Set();
176
+ for (const row of rows) {
177
+ if (!isRecord(row)) continue;
178
+ if (isValidKey(row._key)) out.add(row._key);
179
+ for (const cell of Array.isArray(row.cells) ? row.cells : []) {
180
+ if (isRecord(cell) && isValidKey(cell._key)) out.add(cell._key);
181
+ }
182
+ }
183
+ return out;
184
+ }
185
+ var textContent = (text, key) => text === "" ? [] : [{
186
+ _type: "block",
187
+ _key: key(),
188
+ style: "normal",
189
+ markDefs: [],
190
+ children: [{ _type: "span", _key: key(), text, marks: [] }]
191
+ }];
192
+ var spanOf = (value) => {
193
+ const n = typeof value === "number" ? value : Number(value);
194
+ return Number.isInteger(n) && n > 1 ? n : void 0;
50
195
  };
51
- function Span({ node, index }) {
52
- const marks = node.marks ?? [];
53
- const colour = marks.find((m) => m.startsWith("color:"))?.slice("color:".length);
54
- let out = node.text ?? "";
55
- for (const m of marks) {
56
- const Tag = MARK_TAGS[m];
57
- if (Tag) out = /* @__PURE__ */ jsx2(Tag, { children: out });
196
+ function normalizeCell(raw, at, mint, read) {
197
+ const [row, col] = at;
198
+ const key = isRecord(raw) && isValidKey(raw._key) ? raw._key : mint("r", row, "c", col);
199
+ const inner = deterministicKeys(`${key}-`);
200
+ if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
201
+ return { _key: key, content: textContent(String(raw), inner) };
58
202
  }
59
- if (colour && /^[a-z0-9-]+$/i.test(colour)) {
60
- out = /* @__PURE__ */ jsx2("span", { className: `bcms-color-${colour}`, children: out });
203
+ if (!isRecord(raw)) return { _key: key, content: [] };
204
+ const span = spanOf(raw.span ?? raw.colSpan ?? raw.colspan);
205
+ const rowSpan = spanOf(raw.rowSpan ?? raw.rowspan);
206
+ return {
207
+ _key: key,
208
+ // `text` is the spelling a string matrix uses once its cells have been given objects; it is
209
+ // read here so a half-migrated value cannot lose its words.
210
+ content: Array.isArray(raw.content) ? read(raw.content, inner) : textContent(typeof raw.text === "string" ? raw.text : "", inner),
211
+ ...raw.header === true ? { header: true } : {},
212
+ ...span === void 0 ? {} : { span },
213
+ ...rowSpan === void 0 ? {} : { rowSpan }
214
+ };
215
+ }
216
+ function normalizeRow(raw, at, mint, read) {
217
+ const cells = Array.isArray(raw) ? raw : isRecord(raw) && Array.isArray(raw.cells) ? raw.cells : [];
218
+ return {
219
+ _key: isRecord(raw) && isValidKey(raw._key) ? raw._key : mint("r", at),
220
+ cells: cells.map((c, col) => normalizeCell(c, [at, col], mint, read)),
221
+ ...isRecord(raw) && raw.head === true ? { head: true } : {}
222
+ };
223
+ }
224
+ function builtInTableToTable(node, key = randomKey) {
225
+ if (!isBuiltInTableBlock(node)) return null;
226
+ const fields = node.fields;
227
+ const raw = isRecord(fields) && Array.isArray(fields.rows) ? fields.rows : [];
228
+ const hasHeader = isRecord(fields) ? fields.hasHeader !== false : true;
229
+ const own = node._key;
230
+ const tableKey = isValidKey(own) ? own : key();
231
+ const mint = keyMinter(tableKey, reservedKeys(raw));
232
+ const rows = raw.map((row, i) => {
233
+ const normalized = normalizeRow(row, i, mint, asIs);
234
+ if (!(hasHeader && i === 0)) return normalized;
235
+ return {
236
+ ...normalized,
237
+ head: true,
238
+ cells: normalized.cells.map((c) => ({ ...c, header: true }))
239
+ };
240
+ });
241
+ const width = rows.reduce((max, r) => Math.max(max, r.cells.length), 0);
242
+ for (const [i, row] of rows.entries()) {
243
+ while (row.cells.length < width) {
244
+ row.cells.push({
245
+ _key: mint("r", i, "c", row.cells.length),
246
+ content: [],
247
+ ...row.head === true ? { header: true } : {}
248
+ });
249
+ }
61
250
  }
62
- return /* @__PURE__ */ jsx2(React.Fragment, { children: out }, node._key ?? index);
251
+ return { _type: PT_TYPES.table, _key: tableKey, rows, legacy: true };
63
252
  }
64
- function PortableText({ doc }) {
65
- return /* @__PURE__ */ jsx2(Fragment, { children: doc.content.map((block, i) => {
66
- const Tag = BLOCK_TAGS[block.style ?? "normal"] ?? "p";
67
- return /* @__PURE__ */ jsx2(Tag, { children: (block.children ?? []).map((c, j) => /* @__PURE__ */ jsx2(Span, { node: c, index: j }, c._key ?? j)) }, block._key ?? i);
68
- }) });
253
+ function migrateTable(node, key = randomKey, read = asIs) {
254
+ const builtIn = builtInTableToTable(node, key);
255
+ if (builtIn) return builtIn;
256
+ if (!isPortableTextTable(node)) return null;
257
+ const rows = node.rows;
258
+ const tableKey = isValidKey(node._key) ? node._key : key();
259
+ const mint = keyMinter(tableKey, reservedKeys(rows));
260
+ return {
261
+ _type: PT_TYPES.table,
262
+ _key: tableKey,
263
+ rows: rows.map((r, i) => normalizeRow(r, i, mint, read)),
264
+ // Carried through: a migrated table that an author saves back is still the same table, and
265
+ // dropping the flag on that save would change the page the next time it renders.
266
+ ...node.legacy === true ? { legacy: true } : {}
267
+ };
268
+ }
269
+
270
+ // ../richtext/src/read.ts
271
+ var isRecord2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
272
+ function normalizePortableText(value, key = randomKey) {
273
+ if (!Array.isArray(value)) return [];
274
+ return value.flatMap((raw) => {
275
+ if (!isRecord2(raw) || typeof raw._type !== "string") return [];
276
+ const table = migrateTable(raw, key, (v, cellKey) => normalizePortableText(v, cellKey));
277
+ if (table) return [table];
278
+ const node = { ...raw, _key: isRecord2(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
279
+ if (node._type === "block") {
280
+ if (!Array.isArray(node.markDefs)) node.markDefs = [];
281
+ if (typeof node.style !== "string") node.style = "normal";
282
+ node.children = (Array.isArray(node.children) ? node.children : []).map((c) => {
283
+ if (!isRecord2(c)) return c;
284
+ const child = { ...c, _key: typeof c._key === "string" && c._key ? c._key : key() };
285
+ if (child._type === "span" && !Array.isArray(child.marks)) child.marks = [];
286
+ return child;
287
+ });
288
+ }
289
+ return [node];
290
+ });
291
+ }
292
+ function readStructuredContent(props) {
293
+ if (!isRecord2(props)) return null;
294
+ const candidate = props.content ?? props.value;
295
+ const arr = Array.isArray(candidate) ? candidate : isRecord2(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
296
+ return arr && arr.length > 0 ? arr : null;
297
+ }
298
+
299
+ // src/blocks/portableText.tsx
300
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
301
+ var TOKEN = /^[a-z0-9-]+$/i;
302
+ function styleObject(css) {
303
+ const out = {};
304
+ for (const declaration of css.split(";")) {
305
+ const at = declaration.indexOf(":");
306
+ if (at < 0) continue;
307
+ const name = declaration.slice(0, at).trim();
308
+ const value = declaration.slice(at + 1).trim();
309
+ if (!name || !value) continue;
310
+ out[name.startsWith("--") ? name : name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = value;
311
+ }
312
+ return out;
313
+ }
314
+ function Bound({
315
+ base,
316
+ nodeKey,
317
+ children
318
+ }) {
319
+ if (!base || !nodeKey) return /* @__PURE__ */ jsx2(Fragment, { children });
320
+ return /* @__PURE__ */ jsx2("div", { "data-bcms-field": `${base}#${nodeKey}`, "data-bcms-kind": "richtext", children });
321
+ }
322
+ function Table({ value }) {
323
+ const components = React.useMemo(buildComponents, []);
324
+ const inline = React.useMemo(
325
+ () => ({ ...components, block: { normal: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children }) } }),
326
+ [components]
327
+ );
328
+ const rows = Array.isArray(value?.rows) ? value.rows : [];
329
+ if (rows.length === 0) return null;
330
+ const cells = (row) => (Array.isArray(row?.cells) ? row.cells : []).map((cell) => {
331
+ const Tag = cell?.header === true ? "th" : "td";
332
+ const span = typeof cell?.span === "number" && cell.span > 1 ? cell.span : void 0;
333
+ const rowSpan = typeof cell?.rowSpan === "number" && cell.rowSpan > 1 ? cell.rowSpan : void 0;
334
+ const content = Array.isArray(cell?.content) ? cell.content : [];
335
+ const lone = content.length === 1 && content[0]._type === "block" && content[0].style === "normal" && !content[0].listItem;
336
+ return /* @__PURE__ */ jsx2(Tag, { ...span ? { colSpan: span } : {}, ...rowSpan ? { rowSpan } : {}, children: /* @__PURE__ */ jsx2(PT, { value: content, components: lone ? inline : components }) }, cell?._key);
337
+ });
338
+ const rendered = rows.map((row) => /* @__PURE__ */ jsx2("tr", { children: cells(row) }, row?._key));
339
+ const headCount = rows.findIndex((r) => r?.head !== true);
340
+ const head = headCount === -1 ? rows.length : headCount;
341
+ return /* @__PURE__ */ jsxs("table", { ...value.legacy === true ? { className: "bcms-table" } : {}, children: [
342
+ head > 0 && /* @__PURE__ */ jsx2("thead", { children: rendered.slice(0, head) }),
343
+ head < rows.length && /* @__PURE__ */ jsx2("tbody", { children: rendered.slice(head) })
344
+ ] });
345
+ }
346
+ function buildComponents() {
347
+ return {
348
+ types: {
349
+ [PT_TYPES.divider]: () => /* @__PURE__ */ jsx2("hr", {}),
350
+ [PT_TYPES.table]: ({ value }) => /* @__PURE__ */ jsx2(Table, { value }),
351
+ [PT_TYPES.code]: ({ value }) => {
352
+ const v = value;
353
+ const lang = typeof v.language === "string" && TOKEN.test(v.language) ? v.language : void 0;
354
+ return /* @__PURE__ */ jsx2("pre", { children: /* @__PURE__ */ jsx2("code", { className: lang ? `language-${lang}` : void 0, children: typeof v.code === "string" ? v.code : "" }) });
355
+ },
356
+ /**
357
+ * A generic embedded block renders as a marker, not as content: its payload references
358
+ * something (a component, a media id) that has to be RESOLVED against the project, and
359
+ * this renderer has no project. A marker keeps the node visible and addressable rather
360
+ * than rendering nothing at all, which is how embedded content used to vanish.
361
+ */
362
+ [PT_TYPES.block]: ({ value }) => {
363
+ const v = value;
364
+ const key = typeof v.schemaKey === "string" ? v.schemaKey : "block";
365
+ return /* @__PURE__ */ jsx2("div", { className: "bcms-block", "data-bcms-schema-key": key });
366
+ },
367
+ [PT_TYPES.mention]: ({ value }) => {
368
+ const v = value;
369
+ return /* @__PURE__ */ jsx2("span", { className: "bcms-mention", children: typeof v.label === "string" ? v.label : "" });
370
+ }
371
+ },
372
+ marks: {
373
+ /**
374
+ * The only attribute this renderer writes from author data. An unsafe href renders the
375
+ * LABEL rather than deleting it — blocking a link must not lose the words.
376
+ */
377
+ [PT_TYPES.link]: ({ value, children }) => {
378
+ const v = value ?? {};
379
+ const href = typeof v.href === "string" ? safeHref(v.href) : null;
380
+ if (!href) return /* @__PURE__ */ jsx2(Fragment, { children });
381
+ const blank = v.target === "_blank";
382
+ return /* @__PURE__ */ jsx2("a", { href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
383
+ },
384
+ /**
385
+ * The site's own inline markup, rendered as itself — the React half of
386
+ * `portableTextToHtml`'s `styled` mark, and it has to stay the same shape: Astro and the
387
+ * editor canvas render through that serialiser while a Next site renders through this
388
+ * one, and two renderers of one value that disagree is the whole bug class.
389
+ *
390
+ * The tag is re-checked against `STYLED_TAGS` and the attribute bag re-filtered, because
391
+ * a markDef is DATA and arrives from imports, the Management API and AI writes. React
392
+ * escapes the values it writes; the allowlist is what keeps a `href` or an `onClick` out
393
+ * of the props entirely. An unrecognised tag renders its CHILDREN — the words are the
394
+ * author's, the wrapper is only the design.
395
+ */
396
+ [PT_TYPES.styled]: ({ value, children }) => {
397
+ const v = value ?? {};
398
+ const tag = typeof v.tag === "string" ? v.tag.toLowerCase() : "";
399
+ if (!isStyledTag(tag)) return /* @__PURE__ */ jsx2(Fragment, { children });
400
+ const Tag = tag;
401
+ const className = typeof v.className === "string" && v.className ? v.className : void 0;
402
+ const attrs = styledAttrEntries(v.attrs);
403
+ const style = attrs.find(([name]) => name === "style")?.[1];
404
+ return /* @__PURE__ */ jsx2(
405
+ Tag,
406
+ {
407
+ className,
408
+ ...Object.fromEntries(attrs.filter(([name]) => name !== "style")),
409
+ ...style ? { style: styleObject(style) } : {},
410
+ children
411
+ }
412
+ );
413
+ },
414
+ highlight: ({ children }) => /* @__PURE__ */ jsx2("mark", { children }),
415
+ "strike-through": ({ children }) => /* @__PURE__ */ jsx2("s", { children }),
416
+ /**
417
+ * A colour mark carries a TOKEN name (`color:brand-accent`), never a baked hex, so the
418
+ * consuming site styles it and a rebrand does not orphan the content.
419
+ */
420
+ color: ({ children, markType }) => {
421
+ const token = markType.startsWith("color:") ? markType.slice(6) : "";
422
+ if (!TOKEN.test(token)) return /* @__PURE__ */ jsx2(Fragment, { children });
423
+ return /* @__PURE__ */ jsx2("span", { className: `bcms-color-${token}`, children });
424
+ }
425
+ },
426
+ /** Unknown marks and types render their children. Never swallow content. */
427
+ unknownMark: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children }),
428
+ unknownType: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children: children ?? null }),
429
+ unknownBlockStyle: ({ children }) => /* @__PURE__ */ jsx2("p", { children })
430
+ };
431
+ }
432
+ function PortableText({ value, bindingBase }) {
433
+ const blocks = React.useMemo(() => normalizePortableText(readValue(value)), [value]);
434
+ const components = React.useMemo(buildComponents, []);
435
+ if (blocks.length === 0) return null;
436
+ if (!bindingBase) return /* @__PURE__ */ jsx2(PT, { value: blocks, components });
437
+ return /* @__PURE__ */ jsx2(Fragment, { children: blocks.map((node) => /* @__PURE__ */ jsx2(Bound, { base: bindingBase, nodeKey: node._key, children: /* @__PURE__ */ jsx2(PT, { value: [node], components }) }, node._key)) });
438
+ }
439
+ function readValue(value) {
440
+ if (Array.isArray(value)) return value;
441
+ if (value && typeof value === "object") {
442
+ const o = value;
443
+ if (Array.isArray(o.value)) return o.value;
444
+ if (Array.isArray(o.content)) return o.content;
445
+ }
446
+ return [];
69
447
  }
70
448
 
71
449
  // src/blocks/Text.tsx
@@ -84,16 +462,16 @@ function Text({
84
462
  right: "text-right"
85
463
  }[align];
86
464
  const cls = `text-muted-foreground leading-relaxed ${alignClass} ${className}`;
87
- if (content && content.content.length > 0) {
88
- return /* @__PURE__ */ jsx3("div", { className: cls, children: /* @__PURE__ */ jsx3(PortableText, { doc: content }) });
89
- }
465
+ const structured = readStructuredContent({ content });
90
466
  return /* @__PURE__ */ jsx3(
91
467
  "div",
92
468
  {
93
469
  className: cls,
94
- "data-bcms-field": field,
95
- "data-bcms-kind": kind,
96
- dangerouslySetInnerHTML: { __html: html }
470
+ ...structured ? {} : { "data-bcms-field": field, "data-bcms-kind": kind },
471
+ children: structured ? /* @__PURE__ */ jsx3(PortableText, { value: structured, bindingBase: field }) : (
472
+ // eslint-disable-next-line react/no-danger
473
+ /* @__PURE__ */ jsx3("span", { dangerouslySetInnerHTML: { __html: html } })
474
+ )
97
475
  }
98
476
  );
99
477
  }
@@ -106,7 +484,7 @@ function cn(...inputs) {
106
484
  }
107
485
 
108
486
  // src/blocks/Image.tsx
109
- import { jsx as jsx4, jsxs } from "react/jsx-runtime";
487
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
110
488
  function Image({
111
489
  src,
112
490
  alt,
@@ -118,7 +496,7 @@ function Image({
118
496
  "data-bcms-field": field,
119
497
  "data-bcms-kind": kind
120
498
  }) {
121
- return /* @__PURE__ */ jsxs("figure", { className: cn("w-full", className), children: [
499
+ return /* @__PURE__ */ jsxs2("figure", { className: cn("w-full", className), children: [
122
500
  /* @__PURE__ */ jsx4(
123
501
  "img",
124
502
  {
@@ -171,7 +549,7 @@ function Button({
171
549
  }
172
550
 
173
551
  // src/blocks/Spacer.tsx
174
- import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
552
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
175
553
  function Spacer({ height = 32, className = "" }) {
176
554
  return /* @__PURE__ */ jsx6(
177
555
  "div",
@@ -179,7 +557,7 @@ function Spacer({ height = 32, className = "" }) {
179
557
  className: `w-full border border-dashed border-muted-foreground/20 rounded flex items-center justify-center ${className}`,
180
558
  style: { height: `${height}px` },
181
559
  "aria-hidden": "true",
182
- children: /* @__PURE__ */ jsxs2("span", { className: "text-xs text-muted-foreground/40 select-none", children: [
560
+ children: /* @__PURE__ */ jsxs3("span", { className: "text-xs text-muted-foreground/40 select-none", children: [
183
561
  height,
184
562
  "px"
185
563
  ] })
@@ -211,7 +589,7 @@ function Columns({
211
589
 
212
590
  // src/blocks/Video.tsx
213
591
  import { useState } from "react";
214
- import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
592
+ import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
215
593
  function getEmbedUrl(url) {
216
594
  const ytMatch = url.match(
217
595
  /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
@@ -254,7 +632,7 @@ function Video({
254
632
  }
255
633
  );
256
634
  }
257
- return /* @__PURE__ */ jsxs3("div", { className: cn("w-full overflow-hidden rounded-md bg-black aspect-video", className), children: [
635
+ return /* @__PURE__ */ jsxs4("div", { className: cn("w-full overflow-hidden rounded-md bg-black aspect-video", className), children: [
258
636
  !isLoaded && poster && // eslint-disable-next-line @next/next/no-img-element
259
637
  /* @__PURE__ */ jsx8(
260
638
  "img",
@@ -301,9 +679,9 @@ function Section({
301
679
  }
302
680
 
303
681
  // src/blocks/Navbar.tsx
304
- import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
682
+ import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
305
683
  function Navbar({ logo, links, cta, className = "" }) {
306
- return /* @__PURE__ */ jsxs4("nav", { className: cn("flex items-center gap-6 py-4", className), children: [
684
+ return /* @__PURE__ */ jsxs5("nav", { className: cn("flex items-center gap-6 py-4", className), children: [
307
685
  logo?.src ? /* @__PURE__ */ jsx10("a", { href: logo.href ?? "/", className: "shrink-0", children: /* @__PURE__ */ jsx10("img", { src: logo.src, alt: logo.alt, className: "h-8 w-auto" }) }) : null,
308
686
  /* @__PURE__ */ jsx10("div", { className: "flex flex-1 gap-5", children: (links ?? []).map((l, i) => /* @__PURE__ */ jsx10("a", { href: l.href, className: "text-inherit no-underline", children: l.label }, i)) }),
309
687
  cta ? /* @__PURE__ */ jsx10(
@@ -318,10 +696,10 @@ function Navbar({ logo, links, cta, className = "" }) {
318
696
  }
319
697
 
320
698
  // src/blocks/Footer.tsx
321
- import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
699
+ import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
322
700
  function Footer({ columns, copyright, className = "" }) {
323
- return /* @__PURE__ */ jsxs5("footer", { className: cn("mt-12 border-t pt-8", className), children: [
324
- /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-12", children: (columns ?? []).map((col, i) => /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-1.5", children: [
701
+ return /* @__PURE__ */ jsxs6("footer", { className: cn("mt-12 border-t pt-8", className), children: [
702
+ /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-12", children: (columns ?? []).map((col, i) => /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-1.5", children: [
325
703
  col.heading ? /* @__PURE__ */ jsx11("h4", { className: "mb-2 font-semibold", children: col.heading }) : null,
326
704
  (col.links ?? []).map((l, j) => /* @__PURE__ */ jsx11("a", { href: l.href, className: "text-neutral-500 no-underline", children: l.label }, j))
327
705
  ] }, i)) }),
@@ -331,13 +709,13 @@ function Footer({ columns, copyright, className = "" }) {
331
709
 
332
710
  // src/blocks/Slider.tsx
333
711
  import { useState as useState2 } from "react";
334
- import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
712
+ import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
335
713
  function Slider({ slides, className = "", renderBlock }) {
336
714
  const [i, setI] = useState2(0);
337
715
  const list = slides ?? [];
338
716
  const go = (n) => setI((n + list.length) % (list.length || 1));
339
717
  const btn = "absolute top-1/2 -translate-y-1/2 flex h-9 w-9 items-center justify-center rounded-full border-0 bg-black/50 text-white cursor-pointer";
340
- return /* @__PURE__ */ jsxs6("div", { className: cn("relative w-full", className), children: [
718
+ return /* @__PURE__ */ jsxs7("div", { className: cn("relative w-full", className), children: [
341
719
  list.map((s, idx) => /* @__PURE__ */ jsx12("div", { hidden: idx !== i, className: "flex flex-col gap-4", children: (s.children ?? []).map((b) => /* @__PURE__ */ jsx12("span", { children: renderBlock ? renderBlock(b) : null }, b.id)) }, s.id)),
342
720
  /* @__PURE__ */ jsx12("button", { type: "button", "aria-label": "Previous", onClick: () => go(i - 1), className: cn(btn, "left-2"), children: "\u2039" }),
343
721
  /* @__PURE__ */ jsx12("button", { type: "button", "aria-label": "Next", onClick: () => go(i + 1), className: cn(btn, "right-2"), children: "\u203A" })
@@ -346,11 +724,11 @@ function Slider({ slides, className = "", renderBlock }) {
346
724
 
347
725
  // src/blocks/Tabs.tsx
348
726
  import { useState as useState3 } from "react";
349
- import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
727
+ import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
350
728
  function Tabs({ tabs, className = "", renderBlock }) {
351
729
  const [active, setActive] = useState3(0);
352
730
  const list = tabs ?? [];
353
- return /* @__PURE__ */ jsxs7("div", { className: cn("w-full", className), children: [
731
+ return /* @__PURE__ */ jsxs8("div", { className: cn("w-full", className), children: [
354
732
  /* @__PURE__ */ jsx13("div", { role: "tablist", className: "mb-4 flex gap-1 border-b", children: list.map((t, idx) => /* @__PURE__ */ jsx13(
355
733
  "button",
356
734
  {
@@ -371,19 +749,19 @@ function Tabs({ tabs, className = "", renderBlock }) {
371
749
  }
372
750
 
373
751
  // src/blocks/Placeholders.tsx
374
- import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
752
+ import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
375
753
  var box = "rounded border border-dashed border-neutral-300 bg-neutral-50 p-4 text-sm text-neutral-500";
376
754
  function ComponentInstance({
377
755
  componentId,
378
756
  className = ""
379
757
  }) {
380
- return /* @__PURE__ */ jsxs8("div", { className: cn(box, className), children: [
758
+ return /* @__PURE__ */ jsxs9("div", { className: cn(box, className), children: [
381
759
  "Component instance: ",
382
760
  /* @__PURE__ */ jsx14("code", { children: componentId })
383
761
  ] });
384
762
  }
385
763
  function FormPlaceholder({ formId, className = "" }) {
386
- return /* @__PURE__ */ jsxs8("div", { className: cn(box, className), children: [
764
+ return /* @__PURE__ */ jsxs9("div", { className: cn(box, className), children: [
387
765
  "Form: ",
388
766
  /* @__PURE__ */ jsx14("code", { children: formId })
389
767
  ] });
@@ -397,6 +775,7 @@ export {
397
775
  Heading,
398
776
  Image,
399
777
  Navbar,
778
+ PortableText,
400
779
  Section,
401
780
  Slider,
402
781
  Spacer,