@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/renderer.js CHANGED
@@ -77,6 +77,14 @@ var PT_TYPES = {
77
77
  gallery: "gallery",
78
78
  /** A table of contents built from the headings — see `PortableTextTableOfContents`. The V2 kit's `<nav data-rt="toc">`. */
79
79
  tableOfContents: "tableOfContents",
80
+ /** A footnote, an INLINE object in body text — see `PortableTextFootnote`. The V2 kit's `<sup data-rt="fn">`. */
81
+ footnote: "footnote",
82
+ /** Columns of text — see `PortableTextColumns`. The V2 kit's `<div data-rt="columns">`; the legacy `builtin:columns` reads the same way. */
83
+ columns: "columns",
84
+ /** An author bio — see `PortableTextAuthorBio`. The V2 kit's `<aside data-rt="author">`. */
85
+ authorBio: "authorBio",
86
+ /** Related posts — see `PortableTextRelatedPosts`. The V2 kit's `<aside data-rt="related">`. */
87
+ relatedPosts: "relatedPosts",
80
88
  /**
81
89
  * A table. Spelled `table`, unprefixed, because that is what every other Portable Text
82
90
  * producer calls one — the Sanity-shaped `{_type:"table", rows}` this platform used to refuse
@@ -106,7 +114,10 @@ var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
106
114
  PT_TYPES.keyTakeaways,
107
115
  PT_TYPES.embed,
108
116
  PT_TYPES.gallery,
109
- PT_TYPES.tableOfContents
117
+ PT_TYPES.tableOfContents,
118
+ PT_TYPES.columns,
119
+ PT_TYPES.authorBio,
120
+ PT_TYPES.relatedPosts
110
121
  ]);
111
122
  var EMBEDDED_COMPONENT_KEY = "component";
112
123
 
@@ -581,6 +592,28 @@ function keyTakeawaysView(value) {
581
592
  return { title, points };
582
593
  }
583
594
 
595
+ // ../richtext/src/image-layout.ts
596
+ import {
597
+ focalInCrop,
598
+ formatImageRect,
599
+ imageValueUrl,
600
+ normalizeImageCrop,
601
+ normalizeImageFocal,
602
+ objectPosition,
603
+ parseImageRect
604
+ } from "@bettercms-ai/image-url";
605
+ function richTextImageFrame(src, crop, focal) {
606
+ const c = normalizeImageCrop(crop);
607
+ const f = normalizeImageFocal(focal);
608
+ if (!c && !f) return { src, crop: null, focal: null, position: null, data: [] };
609
+ const drawn = imageValueUrl({ url: src, crop: c, focal: f }) ?? src;
610
+ const data = [];
611
+ if (drawn !== src) data.push(["data-src", src]);
612
+ if (c) data.push(["data-crop", formatImageRect(c)]);
613
+ if (f) data.push(["data-focal", `${f.x},${f.y}`]);
614
+ return { src: drawn, crop: c, focal: f, position: f ? objectPosition(focalInCrop(f, c)) : null, data };
615
+ }
616
+
584
617
  // ../richtext/src/gallery.ts
585
618
  var isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
586
619
  var galleryMediaSrc = (raw) => safeHref(raw) ?? (raw !== "" && !raw.startsWith("//") && !/[:\s]/.test(raw) ? raw : null);
@@ -606,9 +639,12 @@ function galleryView(value) {
606
639
  if (!isRecord3(item) || !isRecord3(item.image)) return;
607
640
  const src = galleryMediaSrc(typeof item.image.src === "string" ? item.image.src.trim() : "");
608
641
  if (!src) return;
642
+ const frame = richTextImageFrame(src, item.image.crop, item.image.focal);
609
643
  images.push({
610
644
  key: typeof item._key === "string" && item._key ? item._key : `i${i}`,
611
- src,
645
+ src: frame.src,
646
+ position: frame.position,
647
+ data: frame.data,
612
648
  alt: typeof item.image.alt === "string" ? item.image.alt : "",
613
649
  caption: typeof item.caption === "string" ? item.caption.trim() : ""
614
650
  });
@@ -616,16 +652,49 @@ function galleryView(value) {
616
652
  return { layout, images };
617
653
  }
618
654
 
619
- // ../richtext/src/component-copy.ts
655
+ // ../richtext/src/columns.ts
656
+ var PT_COLUMNS_LIMITS = { min: 2, max: 4 };
657
+ var BUILTIN_COLUMNS_KEY = "builtin:columns";
620
658
  var isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
621
- var fieldsOf = (value) => isRecord4(value) && value.schemaKey === EMBEDDED_COMPONENT_KEY && isRecord4(value.fields) ? value.fields : null;
659
+ var COLUMNS_STYLES = {
660
+ s: { row: "display:flex;flex-wrap:wrap;gap:0.75rem", col: "flex:1 1 14rem;min-width:0" },
661
+ m: { row: "display:flex;flex-wrap:wrap;gap:1.25rem", col: "flex:1 1 14rem;min-width:0" },
662
+ l: { row: "display:flex;flex-wrap:wrap;gap:2rem", col: "flex:1 1 14rem;min-width:0" }
663
+ };
664
+ 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: [] }] }));
665
+ function columnsView(node) {
666
+ if (!isRecord4(node)) return null;
667
+ if (node._type === PT_TYPES.columns) {
668
+ const gap = node.gap === "s" || node.gap === "l" ? node.gap : "m";
669
+ const columns = (Array.isArray(node.columns) ? node.columns : []).map((c, i) => ({
670
+ key: isRecord4(c) && typeof c._key === "string" && c._key ? c._key : `c${i}`,
671
+ content: isRecord4(c) && Array.isArray(c.content) ? c.content : []
672
+ }));
673
+ return { gap, columns };
674
+ }
675
+ if (node._type === PT_TYPES.block && node.schemaKey === BUILTIN_COLUMNS_KEY) {
676
+ const f = isRecord4(node.fields) ? node.fields : {};
677
+ if (!Array.isArray(f.columns) || !f.columns.every((c) => typeof c === "string")) return null;
678
+ const gap = f.gap === "small" ? "s" : f.gap === "large" ? "l" : "m";
679
+ const columns = (Array.isArray(f.columns) ? f.columns : []).slice(0, PT_COLUMNS_LIMITS.max).map((c, i) => ({
680
+ key: `c${i}`,
681
+ content: legacyContent(typeof c === "string" ? c : "", `c${i}`)
682
+ }));
683
+ return { gap, columns };
684
+ }
685
+ return null;
686
+ }
687
+
688
+ // ../richtext/src/component-copy.ts
689
+ var isRecord5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
690
+ var fieldsOf = (value) => isRecord5(value) && value.schemaKey === EMBEDDED_COMPONENT_KEY && isRecord5(value.fields) ? value.fields : null;
622
691
  function detachedComponentCopy(value) {
623
692
  const f = fieldsOf(value);
624
693
  if (!f || f.detached !== true || !Array.isArray(f.blocks)) return null;
625
694
  return {
626
- id: isRecord4(value) && typeof value._key === "string" ? value._key : "",
695
+ id: isRecord5(value) && typeof value._key === "string" ? value._key : "",
627
696
  componentId: typeof f.componentId === "string" ? f.componentId : "",
628
- props: isRecord4(f.props) ? f.props : {},
697
+ props: isRecord5(f.props) ? f.props : {},
629
698
  blocks: f.blocks,
630
699
  ...typeof f.sourcePublishedAt === "string" ? { sourcePublishedAt: f.sourcePublishedAt } : {},
631
700
  ...typeof f.variant === "string" ? { variant: f.variant } : {}
@@ -638,7 +707,7 @@ function componentCopyLines(copy) {
638
707
  const t = v.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
639
708
  return t || null;
640
709
  }
641
- if (isRecord4(v)) return line(v.label ?? v.text ?? v.title);
710
+ if (isRecord5(v)) return line(v.label ?? v.text ?? v.title);
642
711
  return null;
643
712
  };
644
713
  return Object.values(copy.props).map(line).filter((s) => !!s);
@@ -669,6 +738,14 @@ function portableTextToPlainText(value) {
669
738
  lines.push(...galleryView(node).images.map((x) => x.caption).filter(Boolean));
670
739
  continue;
671
740
  }
741
+ const cols = columnsView(node);
742
+ if (cols) {
743
+ for (const c of cols.columns) {
744
+ const text = portableTextToPlainText(c.content);
745
+ if (text) lines.push(text);
746
+ }
747
+ continue;
748
+ }
672
749
  const table = migrateTable(node);
673
750
  if (table) {
674
751
  lines.push(...tableLines(table));
@@ -709,20 +786,30 @@ var pullQuoteLines = (v) => {
709
786
 
710
787
  // ../richtext/src/toc.ts
711
788
  import { slugify } from "@bettercms-ai/types";
712
- var isRecord5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
713
- var headingText = (node) => (Array.isArray(node.children) ? node.children : []).map((c) => isRecord5(c) && typeof c.text === "string" ? c.text : "").join("").replace(/​/g, "").trim();
789
+ var isRecord6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
790
+ var isHeadingAnchor = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_:.-]{0,127}$/.test(v);
791
+ var headingText = (node) => (Array.isArray(node.children) ? node.children : []).map((c) => isRecord6(c) && typeof c.text === "string" ? c.text : "").join("").replace(/​/g, "").trim();
714
792
  function outlineOf(value) {
715
- const items = [];
716
- const nodes = /* @__PURE__ */ new WeakMap();
793
+ const heads = [];
717
794
  const used = /* @__PURE__ */ new Set();
718
795
  for (const node of Array.isArray(value) ? value : []) {
719
- if (!isRecord5(node) || node._type !== "block" || node.listItem || node.style !== "h2" && node.style !== "h3") continue;
796
+ if (!isRecord6(node) || node._type !== "block" || node.listItem || node.style !== "h2" && node.style !== "h3") continue;
720
797
  const text = headingText(node);
721
798
  if (!text) continue;
722
- const base = slugify(text);
723
- let id = base;
724
- for (let n = 2; used.has(id); n++) id = `${base}-${n}`;
725
- used.add(id);
799
+ const anchor = isHeadingAnchor(node.anchor) && !used.has(node.anchor) ? node.anchor : null;
800
+ if (anchor) used.add(anchor);
801
+ heads.push({ node, text, anchor });
802
+ }
803
+ const items = [];
804
+ const nodes = /* @__PURE__ */ new WeakMap();
805
+ for (const { node, text, anchor } of heads) {
806
+ let id = anchor;
807
+ if (!id) {
808
+ const base = slugify(text);
809
+ id = base;
810
+ for (let n = 2; used.has(id); n++) id = `${base}-${n}`;
811
+ used.add(id);
812
+ }
726
813
  nodes.set(node, id);
727
814
  items.push({ id, text, depth: node.style === "h2" ? 2 : 3 });
728
815
  }
@@ -739,25 +826,79 @@ function tocTree(items, depth) {
739
826
  return out;
740
827
  }
741
828
  function tocView(value) {
742
- const v = isRecord5(value) ? value : {};
829
+ const v = isRecord6(value) ? value : {};
743
830
  const title = typeof v.title === "string" ? v.title.trim() : "";
744
831
  return { title, label: title || "Table of contents", depth: v.depth === "2" ? "2" : "3" };
745
832
  }
746
833
 
834
+ // ../richtext/src/footnote.ts
835
+ var isRecord7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
836
+ function footnoteOrder(value) {
837
+ const notes = [];
838
+ const nodes = /* @__PURE__ */ new WeakMap();
839
+ for (const node of Array.isArray(value) ? value : []) {
840
+ if (!isRecord7(node) || node._type !== "block" || !Array.isArray(node.children)) continue;
841
+ for (const child of node.children) {
842
+ if (!isRecord7(child) || child._type !== PT_TYPES.footnote) continue;
843
+ nodes.set(child, notes.push(typeof child.note === "string" ? child.note : ""));
844
+ }
845
+ }
846
+ return { notes, nodes };
847
+ }
848
+
849
+ // ../richtext/src/references.ts
850
+ var PT_RELATED_LIMITS = { titleChars: 40, minCount: 1, maxCount: 6, posts: 6 };
851
+ var isRecord8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
852
+ var refOf = (v) => isRecord8(v) && typeof v._ref === "string" ? v._ref : "";
853
+ var imageSrc = (raw) => safeHref(raw) ?? (raw !== "" && !raw.startsWith("//") && !/[:\s]/.test(raw) ? raw : null);
854
+ function relatedPostsView(value, resolved = {}) {
855
+ const v = isRecord8(value) ? value : {};
856
+ const title = typeof v.title === "string" ? v.title.trim() : "";
857
+ const mode = v.mode === "manual" || v.mode === "newest" ? v.mode : "topic";
858
+ const n = typeof v.count === "number" && Number.isInteger(v.count) ? v.count : 3;
859
+ const count = Math.min(PT_RELATED_LIMITS.maxCount, Math.max(PT_RELATED_LIMITS.minCount, n));
860
+ const picked = (Array.isArray(v.posts) ? v.posts : []).map(refOf).filter(Boolean);
861
+ const key = typeof v._key === "string" ? v._key : "";
862
+ const ids = mode === "manual" ? picked : (resolved.related?.[key] ?? []).slice(0, count);
863
+ const items = [];
864
+ for (const id of ids) {
865
+ const post = resolved.posts?.[id];
866
+ const url = post && typeof post.url === "string" ? safeHref(post.url) : null;
867
+ if (post && typeof post.title === "string" && url) items.push({ ...post, id, url });
868
+ }
869
+ return { key, title, mode, count, picked, items };
870
+ }
871
+ function authorBioView(value, resolved = {}) {
872
+ const key = isRecord8(value) && typeof value._key === "string" ? value._key : "";
873
+ const id = refOf(isRecord8(value) ? value.author : void 0);
874
+ const a = id ? resolved.authors?.[id] : void 0;
875
+ if (!a || typeof a.name !== "string") return { key, id, author: null };
876
+ const avatar = typeof a.avatar === "string" ? imageSrc(a.avatar.trim()) : null;
877
+ return {
878
+ key,
879
+ id,
880
+ author: {
881
+ name: a.name,
882
+ ...avatar ? { avatar } : {},
883
+ ...typeof a.bio === "string" && a.bio.trim() ? { bio: a.bio.trim() } : {}
884
+ }
885
+ };
886
+ }
887
+
747
888
  // ../richtext/src/read.ts
748
- var isRecord6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
889
+ var isRecord9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
749
890
  function normalizePortableText(value, key = randomKey) {
750
891
  if (!Array.isArray(value)) return [];
751
892
  return value.flatMap((raw) => {
752
- if (!isRecord6(raw) || typeof raw._type !== "string") return [];
893
+ if (!isRecord9(raw) || typeof raw._type !== "string") return [];
753
894
  const table = migrateTable(raw, key, (v, cellKey) => normalizePortableText(v, cellKey));
754
895
  if (table) return [table];
755
- const node = { ...raw, _key: isRecord6(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
896
+ const node = { ...raw, _key: isRecord9(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
756
897
  if (node._type === "block") {
757
898
  if (!Array.isArray(node.markDefs)) node.markDefs = [];
758
899
  if (typeof node.style !== "string") node.style = "normal";
759
900
  node.children = (Array.isArray(node.children) ? node.children : []).map((c) => {
760
- if (!isRecord6(c)) return c;
901
+ if (!isRecord9(c)) return c;
761
902
  const child = { ...c, _key: typeof c._key === "string" && c._key ? c._key : key() };
762
903
  if (child._type === "span" && !Array.isArray(child.marks)) child.marks = [];
763
904
  return child;
@@ -767,9 +908,9 @@ function normalizePortableText(value, key = randomKey) {
767
908
  });
768
909
  }
769
910
  function readStructuredContent(props) {
770
- if (!isRecord6(props)) return null;
911
+ if (!isRecord9(props)) return null;
771
912
  const candidate = props.content ?? props.value;
772
- const arr = Array.isArray(candidate) ? candidate : isRecord6(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
913
+ const arr = Array.isArray(candidate) ? candidate : isRecord9(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
773
914
  return arr && arr.length > 0 ? arr : null;
774
915
  }
775
916
 
@@ -1126,6 +1267,47 @@ function XCard({ url, card }) {
1126
1267
  /* @__PURE__ */ jsx2("a", { href: url, children: card.date || "View on X" })
1127
1268
  ] });
1128
1269
  }
1270
+ function RelatedPosts({ value, resolved }) {
1271
+ const { key, title, mode, count, picked, items } = relatedPostsView(value, resolved);
1272
+ if (!resolved) {
1273
+ return /* @__PURE__ */ jsx2("aside", { "data-rt": "related", "data-key": key, "data-mode": mode, "data-count": count, "data-posts": picked.join(","), "data-title": title || void 0 });
1274
+ }
1275
+ if (!items.length) return null;
1276
+ return /* @__PURE__ */ jsxs("aside", { "data-rt": "related", "data-mode": mode, "data-count": count, "data-posts": picked.join(","), children: [
1277
+ title ? /* @__PURE__ */ jsx2("p", { "data-title": "", children: title }) : null,
1278
+ items.length ? /* @__PURE__ */ jsx2("ul", { children: items.map((p) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsx2("a", { href: p.url, children: p.title }) }, p.id)) }) : null
1279
+ ] });
1280
+ }
1281
+ function AuthorBio({ value, resolved }) {
1282
+ const { key, id, author } = authorBioView(value, resolved);
1283
+ if (!resolved) return /* @__PURE__ */ jsx2("aside", { "data-rt": "author", "data-key": key, "data-author": id });
1284
+ if (!author) return null;
1285
+ return /* @__PURE__ */ jsxs("aside", { "data-rt": "author", "data-author": id, children: [
1286
+ author?.avatar ? /* @__PURE__ */ jsx2("img", { src: author.avatar, alt: "" }) : null,
1287
+ author ? /* @__PURE__ */ jsx2("p", { "data-name": "", children: author.name }) : null,
1288
+ author?.bio ? /* @__PURE__ */ jsx2("p", { "data-bio": "", children: author.bio }) : null
1289
+ ] });
1290
+ }
1291
+ function Columns({ value, components }) {
1292
+ const view = columnsView(value);
1293
+ if (!view) return null;
1294
+ const s = COLUMNS_STYLES[view.gap];
1295
+ return /* @__PURE__ */ jsx2("div", { "data-rt": "columns", "data-gap": view.gap, "data-cols": view.columns.length, style: styleObject(s.row), children: view.columns.map((c) => /* @__PURE__ */ jsx2("div", { "data-col": "true", style: styleObject(s.col), children: c.content.length ? /* @__PURE__ */ jsx2(PT, { value: c.content, components }) : null }, c.key)) });
1296
+ }
1297
+ function FootnoteMark({ value, footnotes }) {
1298
+ const note = typeof value.note === "string" ? value.note : "";
1299
+ const n = footnotes.nodes.get(value);
1300
+ if (!n) return /* @__PURE__ */ jsx2("sup", { "data-rt": "fn", "data-note": note });
1301
+ return /* @__PURE__ */ jsx2("sup", { "data-rt": "fn", id: `fnref-${n}`, "data-note": note, children: /* @__PURE__ */ jsx2("a", { href: `#fn-${n}`, "aria-label": `Footnote ${n}`, children: n }) });
1302
+ }
1303
+ function FootnoteList({ notes }) {
1304
+ if (!notes.length) return null;
1305
+ return /* @__PURE__ */ jsx2("ol", { className: "cs-footnotes", "data-rt": "footnotes", "aria-label": "Footnotes", children: notes.map((note, i) => /* @__PURE__ */ jsxs("li", { id: `fn-${i + 1}`, children: [
1306
+ note,
1307
+ " ",
1308
+ /* @__PURE__ */ jsx2("a", { href: `#fnref-${i + 1}`, "aria-label": `Back to reference ${i + 1}`, children: "\u21A9" })
1309
+ ] }, i)) });
1310
+ }
1129
1311
  function Toc({ value, outline }) {
1130
1312
  const { title, label, depth } = tocView(value);
1131
1313
  const link = (o) => /* @__PURE__ */ jsx2("a", { href: `#${o.id}`, children: o.text });
@@ -1141,7 +1323,16 @@ function Gallery({ value }) {
1141
1323
  const { layout, images } = galleryView(value);
1142
1324
  const s = GALLERY_STYLES[layout];
1143
1325
  return /* @__PURE__ */ jsx2("figure", { "data-rt": "gallery", "data-layout": layout, children: /* @__PURE__ */ jsx2("ul", { style: styleObject(s.ul), tabIndex: layout === "carousel" ? 0 : void 0, children: images.map((x) => /* @__PURE__ */ jsx2("li", { style: styleObject(s.li), children: /* @__PURE__ */ jsxs("figure", { style: styleObject(s.figure), children: [
1144
- /* @__PURE__ */ jsx2("img", { src: x.src, alt: x.alt, style: styleObject(s.img), loading: "lazy" }),
1326
+ /* @__PURE__ */ jsx2(
1327
+ "img",
1328
+ {
1329
+ alt: x.alt,
1330
+ style: styleObject(x.position ? `${s.img};object-position:${x.position}` : s.img),
1331
+ loading: "lazy",
1332
+ ...Object.fromEntries(x.data),
1333
+ src: x.src
1334
+ }
1335
+ ),
1145
1336
  x.caption ? /* @__PURE__ */ jsx2("figcaption", { children: x.caption }) : null
1146
1337
  ] }) }, x.key)) }) });
1147
1338
  }
@@ -1165,8 +1356,8 @@ function Embed({ value, embeds }) {
1165
1356
  }
1166
1357
  return /* @__PURE__ */ jsx2("figure", { "data-rt": "embed", "data-url": url, "data-provider": r.provider, "data-aspect": aspect, "aria-label": title, children: body });
1167
1358
  }
1168
- function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes: /* @__PURE__ */ new WeakMap() }) {
1169
- return {
1359
+ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes: /* @__PURE__ */ new WeakMap() }, footnotes = { notes: [], nodes: /* @__PURE__ */ new WeakMap() }, resolved) {
1360
+ const components = {
1170
1361
  // RT5: every top-level h2 and h3 carries its outline id, as html.ts writes it.
1171
1362
  block: {
1172
1363
  h2: ({ value, children }) => /* @__PURE__ */ jsx2("h2", { id: outline.nodes.get(value), children }),
@@ -1194,6 +1385,7 @@ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes:
1194
1385
  if (rendered !== void 0) return /* @__PURE__ */ jsx2(Fragment, { children: rendered });
1195
1386
  const builtIn = calloutOrToggle(value);
1196
1387
  if (builtIn) return builtIn;
1388
+ if (columnsView(value)) return /* @__PURE__ */ jsx2(Columns, { value, components });
1197
1389
  const copy = detachedComponentCopy(value);
1198
1390
  if (copy) {
1199
1391
  return /* @__PURE__ */ jsx2("div", { className: "bcms-block bcms-block--component", "data-bcms-component-copy": copy.componentId, "data-bcms-instance": copy.id, children: /* @__PURE__ */ jsx2(BlocksRenderer, { blocks: copy.blocks }) });
@@ -1226,6 +1418,9 @@ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes:
1226
1418
  [PT_TYPES.embed]: ({ value }) => /* @__PURE__ */ jsx2(Embed, { value, embeds }),
1227
1419
  [PT_TYPES.gallery]: ({ value }) => /* @__PURE__ */ jsx2(Gallery, { value }),
1228
1420
  [PT_TYPES.tableOfContents]: ({ value }) => /* @__PURE__ */ jsx2(Toc, { value, outline }),
1421
+ [PT_TYPES.columns]: ({ value }) => /* @__PURE__ */ jsx2(Columns, { value, components }),
1422
+ [PT_TYPES.relatedPosts]: ({ value }) => /* @__PURE__ */ jsx2(RelatedPosts, { value, resolved }),
1423
+ [PT_TYPES.authorBio]: ({ value }) => /* @__PURE__ */ jsx2(AuthorBio, { value, resolved }),
1229
1424
  /**
1230
1425
  * Key takeaways — the React half of `portableTextToHtml`'s, and the same DOM: the V2 kit's
1231
1426
  * `<aside data-rt="takeaways">`, read through the one `keyTakeawaysView` both renderers share.
@@ -1238,6 +1433,7 @@ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes:
1238
1433
  /* @__PURE__ */ jsx2("ul", { children: points.map((p, i) => /* @__PURE__ */ jsx2("li", { children: p }, i)) })
1239
1434
  ] });
1240
1435
  },
1436
+ [PT_TYPES.footnote]: ({ value }) => /* @__PURE__ */ jsx2(FootnoteMark, { value, footnotes }),
1241
1437
  [PT_TYPES.mention]: ({ value }) => {
1242
1438
  const v = value;
1243
1439
  return /* @__PURE__ */ jsx2("span", { className: "bcms-mention", children: typeof v.label === "string" ? v.label : "" });
@@ -1302,6 +1498,7 @@ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes:
1302
1498
  unknownType: ({ children }) => /* @__PURE__ */ jsx2(Fragment, { children: children ?? null }),
1303
1499
  unknownBlockStyle: ({ children }) => /* @__PURE__ */ jsx2("p", { children })
1304
1500
  };
1501
+ return components;
1305
1502
  }
1306
1503
  var isListItem = (node) => node._type === "block" && typeof node.listItem === "string" && !!node.listItem;
1307
1504
  function runsOf(blocks) {
@@ -1342,10 +1539,14 @@ function calloutOrToggle(value) {
1342
1539
  }
1343
1540
  return null;
1344
1541
  }
1345
- function PortableText({ value, bindingBase, renderBlock, embeds }) {
1542
+ function PortableText({ value, bindingBase, renderBlock, embeds, resolved }) {
1346
1543
  const blocks = React.useMemo(() => normalizePortableText(readValue(value)), [value]);
1347
1544
  const outline = React.useMemo(() => outlineOf(blocks), [blocks]);
1348
- const components = React.useMemo(() => buildComponents(renderBlock, embeds, outline), [renderBlock, embeds, outline]);
1545
+ const footnotes = React.useMemo(() => footnoteOrder(blocks), [blocks]);
1546
+ const components = React.useMemo(
1547
+ () => buildComponents(renderBlock, embeds, outline, footnotes, resolved),
1548
+ [renderBlock, embeds, outline, footnotes, resolved]
1549
+ );
1349
1550
  const listComponents = React.useMemo(() => {
1350
1551
  if (!bindingBase) return null;
1351
1552
  const keys = new Set(blocks.map((b) => b._key));
@@ -1361,10 +1562,19 @@ function PortableText({ value, bindingBase, renderBlock, embeds }) {
1361
1562
  };
1362
1563
  }, [bindingBase, blocks, components]);
1363
1564
  if (blocks.length === 0) return null;
1364
- if (!bindingBase || !listComponents) return /* @__PURE__ */ jsx2(PT, { value: blocks, components });
1365
- return /* @__PURE__ */ jsx2(Fragment, { children: runsOf(blocks).map(
1366
- (run) => isListItem(run[0]) ? /* @__PURE__ */ jsx2(PT, { value: run, components: listComponents }, run[0]._key) : /* @__PURE__ */ jsx2(Bound, { base: bindingBase, nodeKey: run[0]._key, children: /* @__PURE__ */ jsx2(PT, { value: run, components }) }, run[0]._key)
1367
- ) });
1565
+ const notes = /* @__PURE__ */ jsx2(FootnoteList, { notes: footnotes.notes });
1566
+ if (!bindingBase || !listComponents) {
1567
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1568
+ /* @__PURE__ */ jsx2(PT, { value: blocks, components }),
1569
+ notes
1570
+ ] });
1571
+ }
1572
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1573
+ runsOf(blocks).map(
1574
+ (run) => isListItem(run[0]) ? /* @__PURE__ */ jsx2(PT, { value: run, components: listComponents }, run[0]._key) : /* @__PURE__ */ jsx2(Bound, { base: bindingBase, nodeKey: run[0]._key, children: /* @__PURE__ */ jsx2(PT, { value: run, components }) }, run[0]._key)
1575
+ ),
1576
+ notes
1577
+ ] });
1368
1578
  }
1369
1579
  function readValue(value) {
1370
1580
  if (Array.isArray(value)) return value;
@@ -1538,7 +1748,7 @@ function Spacer({ height = 32, className = "" }) {
1538
1748
  // src/blocks/Columns.tsx
1539
1749
  import "react";
1540
1750
  import { jsx as jsx7 } from "react/jsx-runtime";
1541
- function Columns({
1751
+ function Columns2({
1542
1752
  columns,
1543
1753
  gap = 16,
1544
1754
  className = "",
@@ -1792,7 +2002,7 @@ var BLOCK_REGISTRY = {
1792
2002
  icon: "minus"
1793
2003
  },
1794
2004
  columns: {
1795
- component: Columns,
2005
+ component: Columns2,
1796
2006
  label: "Columns",
1797
2007
  icon: "columns"
1798
2008
  },