@bettercms-ai/ui 0.22.0 → 0.23.1

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/dist/index.js CHANGED
@@ -626,6 +626,14 @@ var PT_TYPES = {
626
626
  gallery: "gallery",
627
627
  /** A table of contents built from the headings — see `PortableTextTableOfContents`. The V2 kit's `<nav data-rt="toc">`. */
628
628
  tableOfContents: "tableOfContents",
629
+ /** A footnote, an INLINE object in body text — see `PortableTextFootnote`. The V2 kit's `<sup data-rt="fn">`. */
630
+ footnote: "footnote",
631
+ /** Columns of text — see `PortableTextColumns`. The V2 kit's `<div data-rt="columns">`; the legacy `builtin:columns` reads the same way. */
632
+ columns: "columns",
633
+ /** An author bio — see `PortableTextAuthorBio`. The V2 kit's `<aside data-rt="author">`. */
634
+ authorBio: "authorBio",
635
+ /** Related posts — see `PortableTextRelatedPosts`. The V2 kit's `<aside data-rt="related">`. */
636
+ relatedPosts: "relatedPosts",
629
637
  /**
630
638
  * A table. Spelled `table`, unprefixed, because that is what every other Portable Text
631
639
  * producer calls one — the Sanity-shaped `{_type:"table", rows}` this platform used to refuse
@@ -655,7 +663,10 @@ var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
655
663
  PT_TYPES.keyTakeaways,
656
664
  PT_TYPES.embed,
657
665
  PT_TYPES.gallery,
658
- PT_TYPES.tableOfContents
666
+ PT_TYPES.tableOfContents,
667
+ PT_TYPES.columns,
668
+ PT_TYPES.authorBio,
669
+ PT_TYPES.relatedPosts
659
670
  ]);
660
671
  var EMBEDDED_COMPONENT_KEY = "component";
661
672
 
@@ -1130,6 +1141,28 @@ function keyTakeawaysView(value) {
1130
1141
  return { title, points };
1131
1142
  }
1132
1143
 
1144
+ // ../richtext/src/image-layout.ts
1145
+ import {
1146
+ focalInCrop,
1147
+ formatImageRect,
1148
+ imageValueUrl,
1149
+ normalizeImageCrop,
1150
+ normalizeImageFocal,
1151
+ objectPosition,
1152
+ parseImageRect
1153
+ } from "@bettercms-ai/image-url";
1154
+ function richTextImageFrame(src, crop, focal) {
1155
+ const c = normalizeImageCrop(crop);
1156
+ const f = normalizeImageFocal(focal);
1157
+ if (!c && !f) return { src, crop: null, focal: null, position: null, data: [] };
1158
+ const drawn = imageValueUrl({ url: src, crop: c, focal: f }) ?? src;
1159
+ const data = [];
1160
+ if (drawn !== src) data.push(["data-src", src]);
1161
+ if (c) data.push(["data-crop", formatImageRect(c)]);
1162
+ if (f) data.push(["data-focal", `${f.x},${f.y}`]);
1163
+ return { src: drawn, crop: c, focal: f, position: f ? objectPosition(focalInCrop(f, c)) : null, data };
1164
+ }
1165
+
1133
1166
  // ../richtext/src/gallery.ts
1134
1167
  var isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1135
1168
  var galleryMediaSrc = (raw) => safeHref(raw) ?? (raw !== "" && !raw.startsWith("//") && !/[:\s]/.test(raw) ? raw : null);
@@ -1155,9 +1188,12 @@ function galleryView(value) {
1155
1188
  if (!isRecord3(item) || !isRecord3(item.image)) return;
1156
1189
  const src = galleryMediaSrc(typeof item.image.src === "string" ? item.image.src.trim() : "");
1157
1190
  if (!src) return;
1191
+ const frame = richTextImageFrame(src, item.image.crop, item.image.focal);
1158
1192
  images.push({
1159
1193
  key: typeof item._key === "string" && item._key ? item._key : `i${i}`,
1160
- src,
1194
+ src: frame.src,
1195
+ position: frame.position,
1196
+ data: frame.data,
1161
1197
  alt: typeof item.image.alt === "string" ? item.image.alt : "",
1162
1198
  caption: typeof item.caption === "string" ? item.caption.trim() : ""
1163
1199
  });
@@ -1165,16 +1201,49 @@ function galleryView(value) {
1165
1201
  return { layout, images };
1166
1202
  }
1167
1203
 
1168
- // ../richtext/src/component-copy.ts
1204
+ // ../richtext/src/columns.ts
1205
+ var PT_COLUMNS_LIMITS = { min: 2, max: 4 };
1206
+ var BUILTIN_COLUMNS_KEY = "builtin:columns";
1169
1207
  var isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1170
- var fieldsOf = (value) => isRecord4(value) && value.schemaKey === EMBEDDED_COMPONENT_KEY && isRecord4(value.fields) ? value.fields : null;
1208
+ var COLUMNS_STYLES = {
1209
+ s: { row: "display:flex;flex-wrap:wrap;gap:0.75rem", col: "flex:1 1 14rem;min-width:0" },
1210
+ m: { row: "display:flex;flex-wrap:wrap;gap:1.25rem", col: "flex:1 1 14rem;min-width:0" },
1211
+ l: { row: "display:flex;flex-wrap:wrap;gap:2rem", col: "flex:1 1 14rem;min-width:0" }
1212
+ };
1213
+ var legacyContent = (text, key) => text.split(/\n{2,}/).filter((p) => p.trim() !== "").map((p, i) => ({ _type: "block", _key: `${key}p${i}`, style: "normal", markDefs: [], children: [{ _type: "span", _key: `${key}s${i}`, text: p, marks: [] }] }));
1214
+ function columnsView(node) {
1215
+ if (!isRecord4(node)) return null;
1216
+ if (node._type === PT_TYPES.columns) {
1217
+ const gap = node.gap === "s" || node.gap === "l" ? node.gap : "m";
1218
+ const columns = (Array.isArray(node.columns) ? node.columns : []).map((c, i) => ({
1219
+ key: isRecord4(c) && typeof c._key === "string" && c._key ? c._key : `c${i}`,
1220
+ content: isRecord4(c) && Array.isArray(c.content) ? c.content : []
1221
+ }));
1222
+ return { gap, columns };
1223
+ }
1224
+ if (node._type === PT_TYPES.block && node.schemaKey === BUILTIN_COLUMNS_KEY) {
1225
+ const f = isRecord4(node.fields) ? node.fields : {};
1226
+ if (!Array.isArray(f.columns) || !f.columns.every((c) => typeof c === "string")) return null;
1227
+ const gap = f.gap === "small" ? "s" : f.gap === "large" ? "l" : "m";
1228
+ const columns = (Array.isArray(f.columns) ? f.columns : []).slice(0, PT_COLUMNS_LIMITS.max).map((c, i) => ({
1229
+ key: `c${i}`,
1230
+ content: legacyContent(typeof c === "string" ? c : "", `c${i}`)
1231
+ }));
1232
+ return { gap, columns };
1233
+ }
1234
+ return null;
1235
+ }
1236
+
1237
+ // ../richtext/src/component-copy.ts
1238
+ var isRecord5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1239
+ var fieldsOf = (value) => isRecord5(value) && value.schemaKey === EMBEDDED_COMPONENT_KEY && isRecord5(value.fields) ? value.fields : null;
1171
1240
  function detachedComponentCopy(value) {
1172
1241
  const f = fieldsOf(value);
1173
1242
  if (!f || f.detached !== true || !Array.isArray(f.blocks)) return null;
1174
1243
  return {
1175
- id: isRecord4(value) && typeof value._key === "string" ? value._key : "",
1244
+ id: isRecord5(value) && typeof value._key === "string" ? value._key : "",
1176
1245
  componentId: typeof f.componentId === "string" ? f.componentId : "",
1177
- props: isRecord4(f.props) ? f.props : {},
1246
+ props: isRecord5(f.props) ? f.props : {},
1178
1247
  blocks: f.blocks,
1179
1248
  ...typeof f.sourcePublishedAt === "string" ? { sourcePublishedAt: f.sourcePublishedAt } : {},
1180
1249
  ...typeof f.variant === "string" ? { variant: f.variant } : {}
@@ -1187,7 +1256,7 @@ function componentCopyLines(copy) {
1187
1256
  const t = v.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
1188
1257
  return t || null;
1189
1258
  }
1190
- if (isRecord4(v)) return line(v.label ?? v.text ?? v.title);
1259
+ if (isRecord5(v)) return line(v.label ?? v.text ?? v.title);
1191
1260
  return null;
1192
1261
  };
1193
1262
  return Object.values(copy.props).map(line).filter((s) => !!s);
@@ -1218,6 +1287,14 @@ function portableTextToPlainText(value) {
1218
1287
  lines.push(...galleryView(node).images.map((x) => x.caption).filter(Boolean));
1219
1288
  continue;
1220
1289
  }
1290
+ const cols = columnsView(node);
1291
+ if (cols) {
1292
+ for (const c of cols.columns) {
1293
+ const text = portableTextToPlainText(c.content);
1294
+ if (text) lines.push(text);
1295
+ }
1296
+ continue;
1297
+ }
1221
1298
  const table = migrateTable(node);
1222
1299
  if (table) {
1223
1300
  lines.push(...tableLines(table));
@@ -1258,20 +1335,30 @@ var pullQuoteLines = (v) => {
1258
1335
 
1259
1336
  // ../richtext/src/toc.ts
1260
1337
  import { slugify } from "@bettercms-ai/types";
1261
- var isRecord5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1262
- var headingText = (node) => (Array.isArray(node.children) ? node.children : []).map((c) => isRecord5(c) && typeof c.text === "string" ? c.text : "").join("").replace(/​/g, "").trim();
1338
+ var isRecord6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1339
+ var isHeadingAnchor = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_:.-]{0,127}$/.test(v);
1340
+ var headingText = (node) => (Array.isArray(node.children) ? node.children : []).map((c) => isRecord6(c) && typeof c.text === "string" ? c.text : "").join("").replace(/​/g, "").trim();
1263
1341
  function outlineOf(value) {
1264
- const items = [];
1265
- const nodes = /* @__PURE__ */ new WeakMap();
1342
+ const heads = [];
1266
1343
  const used = /* @__PURE__ */ new Set();
1267
1344
  for (const node of Array.isArray(value) ? value : []) {
1268
- if (!isRecord5(node) || node._type !== "block" || node.listItem || node.style !== "h2" && node.style !== "h3") continue;
1345
+ if (!isRecord6(node) || node._type !== "block" || node.listItem || node.style !== "h2" && node.style !== "h3") continue;
1269
1346
  const text = headingText(node);
1270
1347
  if (!text) continue;
1271
- const base = slugify(text);
1272
- let id = base;
1273
- for (let n = 2; used.has(id); n++) id = `${base}-${n}`;
1274
- used.add(id);
1348
+ const anchor = isHeadingAnchor(node.anchor) && !used.has(node.anchor) ? node.anchor : null;
1349
+ if (anchor) used.add(anchor);
1350
+ heads.push({ node, text, anchor });
1351
+ }
1352
+ const items = [];
1353
+ const nodes = /* @__PURE__ */ new WeakMap();
1354
+ for (const { node, text, anchor } of heads) {
1355
+ let id = anchor;
1356
+ if (!id) {
1357
+ const base = slugify(text);
1358
+ id = base;
1359
+ for (let n = 2; used.has(id); n++) id = `${base}-${n}`;
1360
+ used.add(id);
1361
+ }
1275
1362
  nodes.set(node, id);
1276
1363
  items.push({ id, text, depth: node.style === "h2" ? 2 : 3 });
1277
1364
  }
@@ -1288,25 +1375,79 @@ function tocTree(items, depth) {
1288
1375
  return out;
1289
1376
  }
1290
1377
  function tocView(value) {
1291
- const v = isRecord5(value) ? value : {};
1378
+ const v = isRecord6(value) ? value : {};
1292
1379
  const title = typeof v.title === "string" ? v.title.trim() : "";
1293
1380
  return { title, label: title || "Table of contents", depth: v.depth === "2" ? "2" : "3" };
1294
1381
  }
1295
1382
 
1383
+ // ../richtext/src/footnote.ts
1384
+ var isRecord7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1385
+ function footnoteOrder(value) {
1386
+ const notes = [];
1387
+ const nodes = /* @__PURE__ */ new WeakMap();
1388
+ for (const node of Array.isArray(value) ? value : []) {
1389
+ if (!isRecord7(node) || node._type !== "block" || !Array.isArray(node.children)) continue;
1390
+ for (const child of node.children) {
1391
+ if (!isRecord7(child) || child._type !== PT_TYPES.footnote) continue;
1392
+ nodes.set(child, notes.push(typeof child.note === "string" ? child.note : ""));
1393
+ }
1394
+ }
1395
+ return { notes, nodes };
1396
+ }
1397
+
1398
+ // ../richtext/src/references.ts
1399
+ var PT_RELATED_LIMITS = { titleChars: 40, minCount: 1, maxCount: 6, posts: 6 };
1400
+ var isRecord8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1401
+ var refOf = (v) => isRecord8(v) && typeof v._ref === "string" ? v._ref : "";
1402
+ var imageSrc = (raw) => safeHref(raw) ?? (raw !== "" && !raw.startsWith("//") && !/[:\s]/.test(raw) ? raw : null);
1403
+ function relatedPostsView(value, resolved = {}) {
1404
+ const v = isRecord8(value) ? value : {};
1405
+ const title = typeof v.title === "string" ? v.title.trim() : "";
1406
+ const mode = v.mode === "manual" || v.mode === "newest" ? v.mode : "topic";
1407
+ const n = typeof v.count === "number" && Number.isInteger(v.count) ? v.count : 3;
1408
+ const count = Math.min(PT_RELATED_LIMITS.maxCount, Math.max(PT_RELATED_LIMITS.minCount, n));
1409
+ const picked = (Array.isArray(v.posts) ? v.posts : []).map(refOf).filter(Boolean);
1410
+ const key = typeof v._key === "string" ? v._key : "";
1411
+ const ids = mode === "manual" ? picked : (resolved.related?.[key] ?? []).slice(0, count);
1412
+ const items = [];
1413
+ for (const id of ids) {
1414
+ const post = resolved.posts?.[id];
1415
+ const url = post && typeof post.url === "string" ? safeHref(post.url) : null;
1416
+ if (post && typeof post.title === "string" && url) items.push({ ...post, id, url });
1417
+ }
1418
+ return { key, title, mode, count, picked, items };
1419
+ }
1420
+ function authorBioView(value, resolved = {}) {
1421
+ const key = isRecord8(value) && typeof value._key === "string" ? value._key : "";
1422
+ const id = refOf(isRecord8(value) ? value.author : void 0);
1423
+ const a = id ? resolved.authors?.[id] : void 0;
1424
+ if (!a || typeof a.name !== "string") return { key, id, author: null };
1425
+ const avatar = typeof a.avatar === "string" ? imageSrc(a.avatar.trim()) : null;
1426
+ return {
1427
+ key,
1428
+ id,
1429
+ author: {
1430
+ name: a.name,
1431
+ ...avatar ? { avatar } : {},
1432
+ ...typeof a.bio === "string" && a.bio.trim() ? { bio: a.bio.trim() } : {}
1433
+ }
1434
+ };
1435
+ }
1436
+
1296
1437
  // ../richtext/src/read.ts
1297
- var isRecord6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1438
+ var isRecord9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1298
1439
  function normalizePortableText(value, key = randomKey) {
1299
1440
  if (!Array.isArray(value)) return [];
1300
1441
  return value.flatMap((raw) => {
1301
- if (!isRecord6(raw) || typeof raw._type !== "string") return [];
1442
+ if (!isRecord9(raw) || typeof raw._type !== "string") return [];
1302
1443
  const table = migrateTable(raw, key, (v, cellKey) => normalizePortableText(v, cellKey));
1303
1444
  if (table) return [table];
1304
- const node = { ...raw, _key: isRecord6(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
1445
+ const node = { ...raw, _key: isRecord9(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
1305
1446
  if (node._type === "block") {
1306
1447
  if (!Array.isArray(node.markDefs)) node.markDefs = [];
1307
1448
  if (typeof node.style !== "string") node.style = "normal";
1308
1449
  node.children = (Array.isArray(node.children) ? node.children : []).map((c) => {
1309
- if (!isRecord6(c)) return c;
1450
+ if (!isRecord9(c)) return c;
1310
1451
  const child = { ...c, _key: typeof c._key === "string" && c._key ? c._key : key() };
1311
1452
  if (child._type === "span" && !Array.isArray(child.marks)) child.marks = [];
1312
1453
  return child;
@@ -1316,9 +1457,9 @@ function normalizePortableText(value, key = randomKey) {
1316
1457
  });
1317
1458
  }
1318
1459
  function readStructuredContent(props) {
1319
- if (!isRecord6(props)) return null;
1460
+ if (!isRecord9(props)) return null;
1320
1461
  const candidate = props.content ?? props.value;
1321
- const arr = Array.isArray(candidate) ? candidate : isRecord6(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
1462
+ const arr = Array.isArray(candidate) ? candidate : isRecord9(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
1322
1463
  return arr && arr.length > 0 ? arr : null;
1323
1464
  }
1324
1465
 
@@ -1675,6 +1816,47 @@ function XCard({ url, card }) {
1675
1816
  /* @__PURE__ */ jsx14("a", { href: url, children: card.date || "View on X" })
1676
1817
  ] });
1677
1818
  }
1819
+ function RelatedPosts({ value, resolved }) {
1820
+ const { key, title, mode, count, picked, items } = relatedPostsView(value, resolved);
1821
+ if (!resolved) {
1822
+ return /* @__PURE__ */ jsx14("aside", { "data-rt": "related", "data-key": key, "data-mode": mode, "data-count": count, "data-posts": picked.join(","), "data-title": title || void 0 });
1823
+ }
1824
+ if (!items.length) return null;
1825
+ return /* @__PURE__ */ jsxs11("aside", { "data-rt": "related", "data-mode": mode, "data-count": count, "data-posts": picked.join(","), children: [
1826
+ title ? /* @__PURE__ */ jsx14("p", { "data-title": "", children: title }) : null,
1827
+ items.length ? /* @__PURE__ */ jsx14("ul", { children: items.map((p) => /* @__PURE__ */ jsx14("li", { children: /* @__PURE__ */ jsx14("a", { href: p.url, children: p.title }) }, p.id)) }) : null
1828
+ ] });
1829
+ }
1830
+ function AuthorBio({ value, resolved }) {
1831
+ const { key, id, author } = authorBioView(value, resolved);
1832
+ if (!resolved) return /* @__PURE__ */ jsx14("aside", { "data-rt": "author", "data-key": key, "data-author": id });
1833
+ if (!author) return null;
1834
+ return /* @__PURE__ */ jsxs11("aside", { "data-rt": "author", "data-author": id, children: [
1835
+ author?.avatar ? /* @__PURE__ */ jsx14("img", { src: author.avatar, alt: "" }) : null,
1836
+ author ? /* @__PURE__ */ jsx14("p", { "data-name": "", children: author.name }) : null,
1837
+ author?.bio ? /* @__PURE__ */ jsx14("p", { "data-bio": "", children: author.bio }) : null
1838
+ ] });
1839
+ }
1840
+ function Columns2({ value, components }) {
1841
+ const view = columnsView(value);
1842
+ if (!view) return null;
1843
+ const s = COLUMNS_STYLES[view.gap];
1844
+ return /* @__PURE__ */ jsx14("div", { "data-rt": "columns", "data-gap": view.gap, "data-cols": view.columns.length, style: styleObject(s.row), children: view.columns.map((c) => /* @__PURE__ */ jsx14("div", { "data-col": "true", style: styleObject(s.col), children: c.content.length ? /* @__PURE__ */ jsx14(PT, { value: c.content, components }) : null }, c.key)) });
1845
+ }
1846
+ function FootnoteMark({ value, footnotes }) {
1847
+ const note = typeof value.note === "string" ? value.note : "";
1848
+ const n = footnotes.nodes.get(value);
1849
+ if (!n) return /* @__PURE__ */ jsx14("sup", { "data-rt": "fn", "data-note": note });
1850
+ return /* @__PURE__ */ jsx14("sup", { "data-rt": "fn", id: `fnref-${n}`, "data-note": note, children: /* @__PURE__ */ jsx14("a", { href: `#fn-${n}`, "aria-label": `Footnote ${n}`, children: n }) });
1851
+ }
1852
+ function FootnoteList({ notes }) {
1853
+ if (!notes.length) return null;
1854
+ return /* @__PURE__ */ jsx14("ol", { className: "cs-footnotes", "data-rt": "footnotes", "aria-label": "Footnotes", children: notes.map((note, i) => /* @__PURE__ */ jsxs11("li", { id: `fn-${i + 1}`, children: [
1855
+ note,
1856
+ " ",
1857
+ /* @__PURE__ */ jsx14("a", { href: `#fnref-${i + 1}`, "aria-label": `Back to reference ${i + 1}`, children: "\u21A9" })
1858
+ ] }, i)) });
1859
+ }
1678
1860
  function Toc({ value, outline }) {
1679
1861
  const { title, label, depth } = tocView(value);
1680
1862
  const link = (o) => /* @__PURE__ */ jsx14("a", { href: `#${o.id}`, children: o.text });
@@ -1690,7 +1872,16 @@ function Gallery({ value }) {
1690
1872
  const { layout, images } = galleryView(value);
1691
1873
  const s = GALLERY_STYLES[layout];
1692
1874
  return /* @__PURE__ */ jsx14("figure", { "data-rt": "gallery", "data-layout": layout, children: /* @__PURE__ */ jsx14("ul", { style: styleObject(s.ul), tabIndex: layout === "carousel" ? 0 : void 0, children: images.map((x) => /* @__PURE__ */ jsx14("li", { style: styleObject(s.li), children: /* @__PURE__ */ jsxs11("figure", { style: styleObject(s.figure), children: [
1693
- /* @__PURE__ */ jsx14("img", { src: x.src, alt: x.alt, style: styleObject(s.img), loading: "lazy" }),
1875
+ /* @__PURE__ */ jsx14(
1876
+ "img",
1877
+ {
1878
+ alt: x.alt,
1879
+ style: styleObject(x.position ? `${s.img};object-position:${x.position}` : s.img),
1880
+ loading: "lazy",
1881
+ ...Object.fromEntries(x.data),
1882
+ src: x.src
1883
+ }
1884
+ ),
1694
1885
  x.caption ? /* @__PURE__ */ jsx14("figcaption", { children: x.caption }) : null
1695
1886
  ] }) }, x.key)) }) });
1696
1887
  }
@@ -1714,8 +1905,8 @@ function Embed({ value, embeds }) {
1714
1905
  }
1715
1906
  return /* @__PURE__ */ jsx14("figure", { "data-rt": "embed", "data-url": url, "data-provider": r.provider, "data-aspect": aspect, "aria-label": title, children: body });
1716
1907
  }
1717
- function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes: /* @__PURE__ */ new WeakMap() }) {
1718
- return {
1908
+ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes: /* @__PURE__ */ new WeakMap() }, footnotes = { notes: [], nodes: /* @__PURE__ */ new WeakMap() }, resolved) {
1909
+ const components = {
1719
1910
  // RT5: every top-level h2 and h3 carries its outline id, as html.ts writes it.
1720
1911
  block: {
1721
1912
  h2: ({ value, children }) => /* @__PURE__ */ jsx14("h2", { id: outline.nodes.get(value), children }),
@@ -1743,6 +1934,7 @@ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes:
1743
1934
  if (rendered !== void 0) return /* @__PURE__ */ jsx14(Fragment2, { children: rendered });
1744
1935
  const builtIn = calloutOrToggle(value);
1745
1936
  if (builtIn) return builtIn;
1937
+ if (columnsView(value)) return /* @__PURE__ */ jsx14(Columns2, { value, components });
1746
1938
  const copy = detachedComponentCopy(value);
1747
1939
  if (copy) {
1748
1940
  return /* @__PURE__ */ jsx14("div", { className: "bcms-block bcms-block--component", "data-bcms-component-copy": copy.componentId, "data-bcms-instance": copy.id, children: /* @__PURE__ */ jsx14(BlocksRenderer, { blocks: copy.blocks }) });
@@ -1775,6 +1967,9 @@ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes:
1775
1967
  [PT_TYPES.embed]: ({ value }) => /* @__PURE__ */ jsx14(Embed, { value, embeds }),
1776
1968
  [PT_TYPES.gallery]: ({ value }) => /* @__PURE__ */ jsx14(Gallery, { value }),
1777
1969
  [PT_TYPES.tableOfContents]: ({ value }) => /* @__PURE__ */ jsx14(Toc, { value, outline }),
1970
+ [PT_TYPES.columns]: ({ value }) => /* @__PURE__ */ jsx14(Columns2, { value, components }),
1971
+ [PT_TYPES.relatedPosts]: ({ value }) => /* @__PURE__ */ jsx14(RelatedPosts, { value, resolved }),
1972
+ [PT_TYPES.authorBio]: ({ value }) => /* @__PURE__ */ jsx14(AuthorBio, { value, resolved }),
1778
1973
  /**
1779
1974
  * Key takeaways — the React half of `portableTextToHtml`'s, and the same DOM: the V2 kit's
1780
1975
  * `<aside data-rt="takeaways">`, read through the one `keyTakeawaysView` both renderers share.
@@ -1787,6 +1982,7 @@ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes:
1787
1982
  /* @__PURE__ */ jsx14("ul", { children: points.map((p, i) => /* @__PURE__ */ jsx14("li", { children: p }, i)) })
1788
1983
  ] });
1789
1984
  },
1985
+ [PT_TYPES.footnote]: ({ value }) => /* @__PURE__ */ jsx14(FootnoteMark, { value, footnotes }),
1790
1986
  [PT_TYPES.mention]: ({ value }) => {
1791
1987
  const v = value;
1792
1988
  return /* @__PURE__ */ jsx14("span", { className: "bcms-mention", children: typeof v.label === "string" ? v.label : "" });
@@ -1851,6 +2047,7 @@ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes:
1851
2047
  unknownType: ({ children }) => /* @__PURE__ */ jsx14(Fragment2, { children: children ?? null }),
1852
2048
  unknownBlockStyle: ({ children }) => /* @__PURE__ */ jsx14("p", { children })
1853
2049
  };
2050
+ return components;
1854
2051
  }
1855
2052
  var isListItem = (node) => node._type === "block" && typeof node.listItem === "string" && !!node.listItem;
1856
2053
  function runsOf(blocks) {
@@ -1891,10 +2088,14 @@ function calloutOrToggle(value) {
1891
2088
  }
1892
2089
  return null;
1893
2090
  }
1894
- function PortableText({ value, bindingBase, renderBlock, embeds }) {
2091
+ function PortableText({ value, bindingBase, renderBlock, embeds, resolved }) {
1895
2092
  const blocks = React6.useMemo(() => normalizePortableText(readValue(value)), [value]);
1896
2093
  const outline = React6.useMemo(() => outlineOf(blocks), [blocks]);
1897
- const components = React6.useMemo(() => buildComponents(renderBlock, embeds, outline), [renderBlock, embeds, outline]);
2094
+ const footnotes = React6.useMemo(() => footnoteOrder(blocks), [blocks]);
2095
+ const components = React6.useMemo(
2096
+ () => buildComponents(renderBlock, embeds, outline, footnotes, resolved),
2097
+ [renderBlock, embeds, outline, footnotes, resolved]
2098
+ );
1898
2099
  const listComponents = React6.useMemo(() => {
1899
2100
  if (!bindingBase) return null;
1900
2101
  const keys = new Set(blocks.map((b) => b._key));
@@ -1910,10 +2111,19 @@ function PortableText({ value, bindingBase, renderBlock, embeds }) {
1910
2111
  };
1911
2112
  }, [bindingBase, blocks, components]);
1912
2113
  if (blocks.length === 0) return null;
1913
- if (!bindingBase || !listComponents) return /* @__PURE__ */ jsx14(PT, { value: blocks, components });
1914
- return /* @__PURE__ */ jsx14(Fragment2, { children: runsOf(blocks).map(
1915
- (run) => isListItem(run[0]) ? /* @__PURE__ */ jsx14(PT, { value: run, components: listComponents }, run[0]._key) : /* @__PURE__ */ jsx14(Bound, { base: bindingBase, nodeKey: run[0]._key, children: /* @__PURE__ */ jsx14(PT, { value: run, components }) }, run[0]._key)
1916
- ) });
2114
+ const notes = /* @__PURE__ */ jsx14(FootnoteList, { notes: footnotes.notes });
2115
+ if (!bindingBase || !listComponents) {
2116
+ return /* @__PURE__ */ jsxs11(Fragment2, { children: [
2117
+ /* @__PURE__ */ jsx14(PT, { value: blocks, components }),
2118
+ notes
2119
+ ] });
2120
+ }
2121
+ return /* @__PURE__ */ jsxs11(Fragment2, { children: [
2122
+ runsOf(blocks).map(
2123
+ (run) => isListItem(run[0]) ? /* @__PURE__ */ jsx14(PT, { value: run, components: listComponents }, run[0]._key) : /* @__PURE__ */ jsx14(Bound, { base: bindingBase, nodeKey: run[0]._key, children: /* @__PURE__ */ jsx14(PT, { value: run, components }) }, run[0]._key)
2124
+ ),
2125
+ notes
2126
+ ] });
1917
2127
  }
1918
2128
  function readValue(value) {
1919
2129
  if (Array.isArray(value)) return value;