@bettercms-ai/ui 0.20.1 → 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/registry.js CHANGED
@@ -183,6 +183,24 @@ var PT_TYPES = {
183
183
  pullQuote: "pullQuote",
184
184
  /** Key takeaways — see `PortableTextKeyTakeaways`. The V2 kit's `<aside data-rt="takeaways">`. */
185
185
  keyTakeaways: "keyTakeaways",
186
+ /**
187
+ * A third-party embed (X, CodePen, Figma, Maps, Spotify, Podcasts, audio, a link card) — see
188
+ * `PortableTextEmbed` and `embed.ts`. The V2 kit's `<figure data-rt="embed">`. Gated by the
189
+ * `embed` feature; YouTube and Vimeo links still resolve here too, through the video resolver.
190
+ */
191
+ embed: "embed",
192
+ /** A gallery of images, a grid or a carousel — see `PortableTextGallery`. The V2 kit's `<figure data-rt="gallery">`. */
193
+ gallery: "gallery",
194
+ /** A table of contents built from the headings — see `PortableTextTableOfContents`. The V2 kit's `<nav data-rt="toc">`. */
195
+ tableOfContents: "tableOfContents",
196
+ /** A footnote, an INLINE object in body text — see `PortableTextFootnote`. The V2 kit's `<sup data-rt="fn">`. */
197
+ footnote: "footnote",
198
+ /** Columns of text — see `PortableTextColumns`. The V2 kit's `<div data-rt="columns">`; the legacy `builtin:columns` reads the same way. */
199
+ columns: "columns",
200
+ /** An author bio — see `PortableTextAuthorBio`. The V2 kit's `<aside data-rt="author">`. */
201
+ authorBio: "authorBio",
202
+ /** Related posts — see `PortableTextRelatedPosts`. The V2 kit's `<aside data-rt="related">`. */
203
+ relatedPosts: "relatedPosts",
186
204
  /**
187
205
  * A table. Spelled `table`, unprefixed, because that is what every other Portable Text
188
206
  * producer calls one — the Sanity-shaped `{_type:"table", rows}` this platform used to refuse
@@ -209,7 +227,13 @@ var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
209
227
  PT_TYPES.video,
210
228
  PT_TYPES.file,
211
229
  PT_TYPES.pullQuote,
212
- PT_TYPES.keyTakeaways
230
+ PT_TYPES.keyTakeaways,
231
+ PT_TYPES.embed,
232
+ PT_TYPES.gallery,
233
+ PT_TYPES.tableOfContents,
234
+ PT_TYPES.columns,
235
+ PT_TYPES.authorBio,
236
+ PT_TYPES.relatedPosts
213
237
  ]);
214
238
  var EMBEDDED_COMPONENT_KEY = "component";
215
239
 
@@ -684,16 +708,109 @@ function keyTakeawaysView(value) {
684
708
  return { title, points };
685
709
  }
686
710
 
687
- // ../richtext/src/component-copy.ts
711
+ // ../richtext/src/image-layout.ts
712
+ import {
713
+ focalInCrop,
714
+ formatImageRect,
715
+ imageValueUrl,
716
+ normalizeImageCrop,
717
+ normalizeImageFocal,
718
+ objectPosition,
719
+ parseImageRect
720
+ } from "@bettercms-ai/image-url";
721
+ function richTextImageFrame(src, crop, focal) {
722
+ const c = normalizeImageCrop(crop);
723
+ const f = normalizeImageFocal(focal);
724
+ if (!c && !f) return { src, crop: null, focal: null, position: null, data: [] };
725
+ const drawn = imageValueUrl({ url: src, crop: c, focal: f }) ?? src;
726
+ const data = [];
727
+ if (drawn !== src) data.push(["data-src", src]);
728
+ if (c) data.push(["data-crop", formatImageRect(c)]);
729
+ if (f) data.push(["data-focal", `${f.x},${f.y}`]);
730
+ return { src: drawn, crop: c, focal: f, position: f ? objectPosition(focalInCrop(f, c)) : null, data };
731
+ }
732
+
733
+ // ../richtext/src/gallery.ts
688
734
  var isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
689
- var fieldsOf = (value) => isRecord3(value) && value.schemaKey === EMBEDDED_COMPONENT_KEY && isRecord3(value.fields) ? value.fields : null;
735
+ var galleryMediaSrc = (raw) => safeHref(raw) ?? (raw !== "" && !raw.startsWith("//") && !/[:\s]/.test(raw) ? raw : null);
736
+ var GALLERY_STYLES = {
737
+ grid: {
738
+ ul: "display:grid;grid-template-columns:repeat(auto-fill,minmax(12rem,1fr));gap:1rem;list-style:none;margin:0;padding:0",
739
+ li: "margin:0",
740
+ figure: "margin:0",
741
+ img: "display:block;width:100%;height:auto"
742
+ },
743
+ carousel: {
744
+ ul: "display:flex;gap:1rem;overflow-x:auto;scroll-snap-type:x mandatory;list-style:none;margin:0;padding:0",
745
+ li: "flex:0 0 100%;scroll-snap-align:start;margin:0",
746
+ figure: "margin:0",
747
+ img: "display:block;width:100%;height:auto"
748
+ }
749
+ };
750
+ function galleryView(value) {
751
+ const v = isRecord3(value) ? value : {};
752
+ const layout = v.layout === "carousel" ? "carousel" : "grid";
753
+ const images = [];
754
+ (Array.isArray(v.images) ? v.images : []).forEach((item, i) => {
755
+ if (!isRecord3(item) || !isRecord3(item.image)) return;
756
+ const src = galleryMediaSrc(typeof item.image.src === "string" ? item.image.src.trim() : "");
757
+ if (!src) return;
758
+ const frame = richTextImageFrame(src, item.image.crop, item.image.focal);
759
+ images.push({
760
+ key: typeof item._key === "string" && item._key ? item._key : `i${i}`,
761
+ src: frame.src,
762
+ position: frame.position,
763
+ data: frame.data,
764
+ alt: typeof item.image.alt === "string" ? item.image.alt : "",
765
+ caption: typeof item.caption === "string" ? item.caption.trim() : ""
766
+ });
767
+ });
768
+ return { layout, images };
769
+ }
770
+
771
+ // ../richtext/src/columns.ts
772
+ var PT_COLUMNS_LIMITS = { min: 2, max: 4 };
773
+ var BUILTIN_COLUMNS_KEY = "builtin:columns";
774
+ var isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
775
+ var COLUMNS_STYLES = {
776
+ s: { row: "display:flex;flex-wrap:wrap;gap:0.75rem", col: "flex:1 1 14rem;min-width:0" },
777
+ m: { row: "display:flex;flex-wrap:wrap;gap:1.25rem", col: "flex:1 1 14rem;min-width:0" },
778
+ l: { row: "display:flex;flex-wrap:wrap;gap:2rem", col: "flex:1 1 14rem;min-width:0" }
779
+ };
780
+ 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: [] }] }));
781
+ function columnsView(node) {
782
+ if (!isRecord4(node)) return null;
783
+ if (node._type === PT_TYPES.columns) {
784
+ const gap = node.gap === "s" || node.gap === "l" ? node.gap : "m";
785
+ const columns = (Array.isArray(node.columns) ? node.columns : []).map((c, i) => ({
786
+ key: isRecord4(c) && typeof c._key === "string" && c._key ? c._key : `c${i}`,
787
+ content: isRecord4(c) && Array.isArray(c.content) ? c.content : []
788
+ }));
789
+ return { gap, columns };
790
+ }
791
+ if (node._type === PT_TYPES.block && node.schemaKey === BUILTIN_COLUMNS_KEY) {
792
+ const f = isRecord4(node.fields) ? node.fields : {};
793
+ if (!Array.isArray(f.columns) || !f.columns.every((c) => typeof c === "string")) return null;
794
+ const gap = f.gap === "small" ? "s" : f.gap === "large" ? "l" : "m";
795
+ const columns = (Array.isArray(f.columns) ? f.columns : []).slice(0, PT_COLUMNS_LIMITS.max).map((c, i) => ({
796
+ key: `c${i}`,
797
+ content: legacyContent(typeof c === "string" ? c : "", `c${i}`)
798
+ }));
799
+ return { gap, columns };
800
+ }
801
+ return null;
802
+ }
803
+
804
+ // ../richtext/src/component-copy.ts
805
+ var isRecord5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
806
+ var fieldsOf = (value) => isRecord5(value) && value.schemaKey === EMBEDDED_COMPONENT_KEY && isRecord5(value.fields) ? value.fields : null;
690
807
  function detachedComponentCopy(value) {
691
808
  const f = fieldsOf(value);
692
809
  if (!f || f.detached !== true || !Array.isArray(f.blocks)) return null;
693
810
  return {
694
- id: isRecord3(value) && typeof value._key === "string" ? value._key : "",
811
+ id: isRecord5(value) && typeof value._key === "string" ? value._key : "",
695
812
  componentId: typeof f.componentId === "string" ? f.componentId : "",
696
- props: isRecord3(f.props) ? f.props : {},
813
+ props: isRecord5(f.props) ? f.props : {},
697
814
  blocks: f.blocks,
698
815
  ...typeof f.sourcePublishedAt === "string" ? { sourcePublishedAt: f.sourcePublishedAt } : {},
699
816
  ...typeof f.variant === "string" ? { variant: f.variant } : {}
@@ -706,7 +823,7 @@ function componentCopyLines(copy) {
706
823
  const t = v.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
707
824
  return t || null;
708
825
  }
709
- if (isRecord3(v)) return line(v.label ?? v.text ?? v.title);
826
+ if (isRecord5(v)) return line(v.label ?? v.text ?? v.title);
710
827
  return null;
711
828
  };
712
829
  return Object.values(copy.props).map(line).filter((s) => !!s);
@@ -733,6 +850,18 @@ function portableTextToPlainText(value) {
733
850
  lines.push(...title ? [title] : [], ...points);
734
851
  continue;
735
852
  }
853
+ if (node._type === PT_TYPES.gallery) {
854
+ lines.push(...galleryView(node).images.map((x) => x.caption).filter(Boolean));
855
+ continue;
856
+ }
857
+ const cols = columnsView(node);
858
+ if (cols) {
859
+ for (const c of cols.columns) {
860
+ const text = portableTextToPlainText(c.content);
861
+ if (text) lines.push(text);
862
+ }
863
+ continue;
864
+ }
736
865
  const table = migrateTable(node);
737
866
  if (table) {
738
867
  lines.push(...tableLines(table));
@@ -771,20 +900,121 @@ var pullQuoteLines = (v) => {
771
900
  return [text(v.quote), ...by ? [`\u2014 ${by}`] : []].filter(Boolean);
772
901
  };
773
902
 
903
+ // ../richtext/src/toc.ts
904
+ import { slugify } from "@bettercms-ai/types";
905
+ var isRecord6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
906
+ var isHeadingAnchor = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_:.-]{0,127}$/.test(v);
907
+ var headingText = (node) => (Array.isArray(node.children) ? node.children : []).map((c) => isRecord6(c) && typeof c.text === "string" ? c.text : "").join("").replace(/​/g, "").trim();
908
+ function outlineOf(value) {
909
+ const heads = [];
910
+ const used = /* @__PURE__ */ new Set();
911
+ for (const node of Array.isArray(value) ? value : []) {
912
+ if (!isRecord6(node) || node._type !== "block" || node.listItem || node.style !== "h2" && node.style !== "h3") continue;
913
+ const text = headingText(node);
914
+ if (!text) continue;
915
+ const anchor = isHeadingAnchor(node.anchor) && !used.has(node.anchor) ? node.anchor : null;
916
+ if (anchor) used.add(anchor);
917
+ heads.push({ node, text, anchor });
918
+ }
919
+ const items = [];
920
+ const nodes = /* @__PURE__ */ new WeakMap();
921
+ for (const { node, text, anchor } of heads) {
922
+ let id = anchor;
923
+ if (!id) {
924
+ const base = slugify(text);
925
+ id = base;
926
+ for (let n = 2; used.has(id); n++) id = `${base}-${n}`;
927
+ used.add(id);
928
+ }
929
+ nodes.set(node, id);
930
+ items.push({ id, text, depth: node.style === "h2" ? 2 : 3 });
931
+ }
932
+ return { items, nodes };
933
+ }
934
+ function tocTree(items, depth) {
935
+ const out = [];
936
+ for (const it of items) {
937
+ if (it.depth === 3 && depth === "2") continue;
938
+ const last = out[out.length - 1];
939
+ if (it.depth === 3 && last) last.children.push(it);
940
+ else out.push({ ...it, children: [] });
941
+ }
942
+ return out;
943
+ }
944
+ function tocView(value) {
945
+ const v = isRecord6(value) ? value : {};
946
+ const title = typeof v.title === "string" ? v.title.trim() : "";
947
+ return { title, label: title || "Table of contents", depth: v.depth === "2" ? "2" : "3" };
948
+ }
949
+
950
+ // ../richtext/src/footnote.ts
951
+ var isRecord7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
952
+ function footnoteOrder(value) {
953
+ const notes = [];
954
+ const nodes = /* @__PURE__ */ new WeakMap();
955
+ for (const node of Array.isArray(value) ? value : []) {
956
+ if (!isRecord7(node) || node._type !== "block" || !Array.isArray(node.children)) continue;
957
+ for (const child of node.children) {
958
+ if (!isRecord7(child) || child._type !== PT_TYPES.footnote) continue;
959
+ nodes.set(child, notes.push(typeof child.note === "string" ? child.note : ""));
960
+ }
961
+ }
962
+ return { notes, nodes };
963
+ }
964
+
965
+ // ../richtext/src/references.ts
966
+ var PT_RELATED_LIMITS = { titleChars: 40, minCount: 1, maxCount: 6, posts: 6 };
967
+ var isRecord8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
968
+ var refOf = (v) => isRecord8(v) && typeof v._ref === "string" ? v._ref : "";
969
+ var imageSrc = (raw) => safeHref(raw) ?? (raw !== "" && !raw.startsWith("//") && !/[:\s]/.test(raw) ? raw : null);
970
+ function relatedPostsView(value, resolved = {}) {
971
+ const v = isRecord8(value) ? value : {};
972
+ const title = typeof v.title === "string" ? v.title.trim() : "";
973
+ const mode = v.mode === "manual" || v.mode === "newest" ? v.mode : "topic";
974
+ const n = typeof v.count === "number" && Number.isInteger(v.count) ? v.count : 3;
975
+ const count = Math.min(PT_RELATED_LIMITS.maxCount, Math.max(PT_RELATED_LIMITS.minCount, n));
976
+ const picked = (Array.isArray(v.posts) ? v.posts : []).map(refOf).filter(Boolean);
977
+ const key = typeof v._key === "string" ? v._key : "";
978
+ const ids = mode === "manual" ? picked : (resolved.related?.[key] ?? []).slice(0, count);
979
+ const items = [];
980
+ for (const id of ids) {
981
+ const post = resolved.posts?.[id];
982
+ const url = post && typeof post.url === "string" ? safeHref(post.url) : null;
983
+ if (post && typeof post.title === "string" && url) items.push({ ...post, id, url });
984
+ }
985
+ return { key, title, mode, count, picked, items };
986
+ }
987
+ function authorBioView(value, resolved = {}) {
988
+ const key = isRecord8(value) && typeof value._key === "string" ? value._key : "";
989
+ const id = refOf(isRecord8(value) ? value.author : void 0);
990
+ const a = id ? resolved.authors?.[id] : void 0;
991
+ if (!a || typeof a.name !== "string") return { key, id, author: null };
992
+ const avatar = typeof a.avatar === "string" ? imageSrc(a.avatar.trim()) : null;
993
+ return {
994
+ key,
995
+ id,
996
+ author: {
997
+ name: a.name,
998
+ ...avatar ? { avatar } : {},
999
+ ...typeof a.bio === "string" && a.bio.trim() ? { bio: a.bio.trim() } : {}
1000
+ }
1001
+ };
1002
+ }
1003
+
774
1004
  // ../richtext/src/read.ts
775
- var isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1005
+ var isRecord9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
776
1006
  function normalizePortableText(value, key = randomKey) {
777
1007
  if (!Array.isArray(value)) return [];
778
1008
  return value.flatMap((raw) => {
779
- if (!isRecord4(raw) || typeof raw._type !== "string") return [];
1009
+ if (!isRecord9(raw) || typeof raw._type !== "string") return [];
780
1010
  const table = migrateTable(raw, key, (v, cellKey) => normalizePortableText(v, cellKey));
781
1011
  if (table) return [table];
782
- const node = { ...raw, _key: isRecord4(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
1012
+ const node = { ...raw, _key: isRecord9(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
783
1013
  if (node._type === "block") {
784
1014
  if (!Array.isArray(node.markDefs)) node.markDefs = [];
785
1015
  if (typeof node.style !== "string") node.style = "normal";
786
1016
  node.children = (Array.isArray(node.children) ? node.children : []).map((c) => {
787
- if (!isRecord4(c)) return c;
1017
+ if (!isRecord9(c)) return c;
788
1018
  const child = { ...c, _key: typeof c._key === "string" && c._key ? c._key : key() };
789
1019
  if (child._type === "span" && !Array.isArray(child.marks)) child.marks = [];
790
1020
  return child;
@@ -794,12 +1024,219 @@ function normalizePortableText(value, key = randomKey) {
794
1024
  });
795
1025
  }
796
1026
  function readStructuredContent(props) {
797
- if (!isRecord4(props)) return null;
1027
+ if (!isRecord9(props)) return null;
798
1028
  const candidate = props.content ?? props.value;
799
- const arr = Array.isArray(candidate) ? candidate : isRecord4(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
1029
+ const arr = Array.isArray(candidate) ? candidate : isRecord9(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
800
1030
  return arr && arr.length > 0 ? arr : null;
801
1031
  }
802
1032
 
1033
+ // ../richtext/src/video.ts
1034
+ var videoId = (value) => value && /^[a-zA-Z0-9_-]{6,32}$/.test(value) ? value : null;
1035
+ function resolveVideoSource(value) {
1036
+ if (typeof value !== "string" || !value.trim()) return null;
1037
+ let parsed;
1038
+ try {
1039
+ parsed = new URL(value.trim());
1040
+ } catch {
1041
+ return null;
1042
+ }
1043
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
1044
+ const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
1045
+ let id = null;
1046
+ if (host === "youtu.be") id = videoId(parsed.pathname.split("/").filter(Boolean)[0]);
1047
+ if (host === "youtube.com" || host === "m.youtube.com" || host === "youtube-nocookie.com") {
1048
+ id = videoId(
1049
+ parsed.searchParams.get("v") ?? parsed.pathname.match(/^\/(?:embed|shorts)\/([^/?#]+)/)?.[1]
1050
+ );
1051
+ }
1052
+ if (id) {
1053
+ return {
1054
+ kind: "iframe",
1055
+ src: `https://www.youtube-nocookie.com/embed/${id}`,
1056
+ provider: "YouTube"
1057
+ };
1058
+ }
1059
+ if (host === "vimeo.com" || host === "player.vimeo.com") {
1060
+ const segments = parsed.pathname.split("/").filter(Boolean);
1061
+ let vimeoId = null;
1062
+ for (let i = segments.length - 1; i >= 0; i -= 1) {
1063
+ if (/^\d+$/.test(segments[i])) {
1064
+ vimeoId = segments[i];
1065
+ break;
1066
+ }
1067
+ }
1068
+ if (vimeoId) {
1069
+ return {
1070
+ kind: "iframe",
1071
+ src: `https://player.vimeo.com/video/${vimeoId}`,
1072
+ provider: "Vimeo"
1073
+ };
1074
+ }
1075
+ }
1076
+ if (/\.(?:m4v|mp4|og[gv]|webm)$/i.test(parsed.pathname)) {
1077
+ return { kind: "video", src: parsed.toString(), provider: "Video file" };
1078
+ }
1079
+ return null;
1080
+ }
1081
+
1082
+ // ../richtext/src/embed.ts
1083
+ var EMBED_ASPECTS = ["16:9", "4:3", "1:1", "9:16"];
1084
+ var isEmbedAspect = (v) => EMBED_ASPECTS.includes(v);
1085
+ function detectEmbed(url) {
1086
+ const u = url.trim();
1087
+ let host = "";
1088
+ try {
1089
+ host = new URL(u).hostname.replace(/^www\./, "");
1090
+ } catch {
1091
+ host = "";
1092
+ }
1093
+ const is = (re) => re.test(u);
1094
+ if (is(/(?:youtube\.com\/(?:watch\?|embed\/|shorts\/|live\/)|youtu\.be\/)/i)) return { provider: "youtube", label: "YouTube", host };
1095
+ if (is(/vimeo\.com\/(?:video\/)?\d+/i)) return { provider: "vimeo", label: "Vimeo", host };
1096
+ if (is(/(?:twitter|x)\.com\/[^/]+\/status\/\d+/i)) return { provider: "x", label: "X post", host };
1097
+ if (is(/codepen\.io\/[^/]+\/(?:pen|embed)\//i)) return { provider: "codepen", label: "CodePen", host };
1098
+ if (is(/figma\.com\/(?:file|design|proto|board)\//i)) return { provider: "figma", label: "Figma", host };
1099
+ if (is(/(?:google\.[a-z.]+\/maps|maps\.app\.goo\.gl|goo\.gl\/maps)/i)) return { provider: "maps", label: "Google Maps", host };
1100
+ if (is(/open\.spotify\.com\/(?:episode|track|show|playlist|album)\//i)) return { provider: "spotify", label: "Spotify", host };
1101
+ if (is(/podcasts\.apple\.com\//i)) return { provider: "podcast", label: "Apple Podcasts", host };
1102
+ if (is(/\.(?:mp3|m4a|wav|ogg|aac)(?:\?|$)/i)) return { provider: "audio", label: "Audio", host };
1103
+ return { provider: "link", label: host || "Link", host };
1104
+ }
1105
+ function detectMore(url) {
1106
+ const host = url.hostname.toLowerCase().replace(/^www\./, "");
1107
+ const path = url.pathname;
1108
+ if (host === "youtube-nocookie.com" && path.startsWith("/embed/")) return { provider: "youtube", label: "YouTube" };
1109
+ if (host === "open.spotify.com" && /^\/embed\/(?:episode|track|show|playlist|album)\//.test(path)) return { provider: "spotify", label: "Spotify" };
1110
+ if (host === "calendly.com") return { provider: "calendly", label: "Calendly" };
1111
+ if (host.endsWith(".typeform.com") && /^\/to\//.test(path)) return { provider: "typeform", label: "Typeform" };
1112
+ if (host === "loom.com" && /^\/(?:share|embed)\//.test(path)) return { provider: "loom", label: "Loom" };
1113
+ if (host === "airtable.com") return { provider: "airtable", label: "Airtable" };
1114
+ if (host === "docs.google.com") {
1115
+ const kind = path.split("/")[1];
1116
+ const label = { forms: "Google Forms", document: "Google Docs", spreadsheets: "Google Sheets", presentation: "Google Slides" }[kind ?? ""];
1117
+ if (label) return { provider: "google", label };
1118
+ }
1119
+ return null;
1120
+ }
1121
+ var FRAME_SIZE = {
1122
+ "16:9": ["640", "360"],
1123
+ "4:3": ["640", "480"],
1124
+ "1:1": ["640", "640"],
1125
+ "9:16": ["360", "640"]
1126
+ };
1127
+ var ID = /^[A-Za-z0-9_-]{1,64}$/;
1128
+ var FULL_WIDTH_HEIGHT = { calendly: "700", typeform: "500", airtable: "533" };
1129
+ function frameOf(provider, url) {
1130
+ const parts = url.pathname.split("/").filter(Boolean);
1131
+ switch (provider) {
1132
+ case "youtube":
1133
+ case "vimeo": {
1134
+ const video = resolveVideoSource(url.href);
1135
+ return video?.kind === "iframe" ? { src: video.src } : null;
1136
+ }
1137
+ case "codepen": {
1138
+ const [user, mode, id] = parts;
1139
+ return user && ID.test(user) && (mode === "pen" || mode === "embed") && id && ID.test(id) ? { src: `https://codepen.io/${user}/embed/${id}?default-tab=result` } : null;
1140
+ }
1141
+ case "figma": {
1142
+ const [mode, key] = parts;
1143
+ if (!mode || !["file", "design", "proto", "board"].includes(mode) || !key || !ID.test(key)) return null;
1144
+ const frame = new URL(`https://embed.figma.com/${mode}/${key}`);
1145
+ const node = url.searchParams.get("node-id");
1146
+ if (node && /^[0-9A-Za-z:_-]{1,64}$/.test(node)) frame.searchParams.set("node-id", node);
1147
+ frame.searchParams.set("embed-host", "bettercms");
1148
+ return { src: frame.href };
1149
+ }
1150
+ case "maps": {
1151
+ if (!/^(?:www\.)?google\.[a-z.]+$/i.test(url.hostname)) return null;
1152
+ const pb = url.searchParams.get("pb");
1153
+ if (parts[0] === "maps" && parts[1] === "embed" && pb && /^[!A-Za-z0-9._:%+-]{1,2000}$/.test(pb)) {
1154
+ return { src: `https://www.google.com/maps/embed?pb=${encodeURIComponent(pb)}` };
1155
+ }
1156
+ const at = /@(-?\d{1,3}(?:\.\d+)?),(-?\d{1,3}(?:\.\d+)?)/.exec(url.pathname);
1157
+ const place = parts[0] === "maps" && parts[1] === "place" && parts[2] ? decodeURIComponent(parts[2]).replace(/\+/g, " ") : null;
1158
+ const q = at ? `${at[1]},${at[2]}` : url.searchParams.get("q") ?? place;
1159
+ if (!q || q.length > 200) return null;
1160
+ return { src: `https://maps.google.com/maps?q=${encodeURIComponent(q)}&output=embed` };
1161
+ }
1162
+ case "spotify": {
1163
+ const [type, id] = parts[0] === "embed" ? parts.slice(1) : parts;
1164
+ if (!type || !["episode", "track", "show", "playlist", "album"].includes(type) || !id || !ID.test(id)) return null;
1165
+ return { src: `https://open.spotify.com/embed/${type}/${id}`, height: type === "track" || type === "episode" ? "152" : "352" };
1166
+ }
1167
+ case "podcast":
1168
+ return (url.hostname === "podcasts.apple.com" || url.hostname === "embed.podcasts.apple.com") && parts.length >= 2 ? { src: `https://embed.podcasts.apple.com${url.pathname}${url.search}`, height: url.searchParams.has("i") ? "175" : "450" } : null;
1169
+ case "calendly":
1170
+ return parts.length >= 1 && parts.length <= 2 && parts.every((p) => ID.test(p)) ? { src: `https://calendly.com/${parts.join("/")}?embed_type=Inline&hide_gdpr_banner=1` } : null;
1171
+ case "typeform": {
1172
+ const id = parts[0] === "to" ? parts[1] : void 0;
1173
+ return id && /^[A-Za-z0-9]{4,32}$/.test(id) ? { src: `https://form.typeform.com/to/${id}` } : null;
1174
+ }
1175
+ case "loom": {
1176
+ const id = parts[1];
1177
+ return (parts[0] === "share" || parts[0] === "embed") && id && /^[0-9a-f]{32}$/.test(id) ? { src: `https://www.loom.com/embed/${id}` } : null;
1178
+ }
1179
+ case "airtable": {
1180
+ const ids = parts[0] === "embed" ? parts.slice(1) : parts;
1181
+ return ids.length >= 1 && ids.length <= 3 && ids.every((p) => /^(?:app|shr|tbl|viw)[A-Za-z0-9]{14}$/.test(p)) && ids.some((p) => p.startsWith("shr")) ? { src: `https://airtable.com/embed/${ids.join("/")}` } : null;
1182
+ }
1183
+ case "google": {
1184
+ const [kind, d, maybeE, maybeId] = parts;
1185
+ if (d !== "d") return null;
1186
+ const published = maybeE === "e";
1187
+ const id = published ? maybeId : maybeE;
1188
+ if (!id || !/^[A-Za-z0-9_-]{10,128}$/.test(id)) return null;
1189
+ const base = `https://docs.google.com/${kind}/d/${published ? `e/${id}` : id}`;
1190
+ if (kind === "forms") return { src: `${base}/viewform?embedded=true`, height: "800" };
1191
+ if (kind === "document") return { src: published ? `${base}/pub?embedded=true` : `${base}/preview`, height: "600" };
1192
+ if (kind === "spreadsheets") return { src: published ? `${base}/pubhtml?widget=true&headers=false` : `${base}/preview`, height: "600" };
1193
+ if (kind === "presentation") return { src: published ? `${base}/embed` : `${base}/preview` };
1194
+ return null;
1195
+ }
1196
+ default:
1197
+ return null;
1198
+ }
1199
+ }
1200
+ var X_STATUS = /^\/([A-Za-z0-9_]{1,15})\/status\/(\d{1,25})(?:\/|$)/;
1201
+ var X_HOSTS = /^(?:www\.|mobile\.)?(?:twitter|x)\.com$/i;
1202
+ function canonicalEmbedUrl(raw) {
1203
+ const url = typeof raw === "string" ? raw.trim() : "";
1204
+ const safe = url ? safeHref(url) : null;
1205
+ if (!safe || !/^https?:\/\//i.test(safe)) return null;
1206
+ let parsed;
1207
+ try {
1208
+ parsed = new URL(safe);
1209
+ } catch {
1210
+ return null;
1211
+ }
1212
+ const status = X_HOSTS.test(parsed.hostname) ? X_STATUS.exec(parsed.pathname) : null;
1213
+ if (status) return `https://x.com/${status[1].toLowerCase()}/status/${status[2]}`;
1214
+ parsed.hash = "";
1215
+ return parsed.href;
1216
+ }
1217
+ function resolveEmbed(rawUrl, aspect = "16:9") {
1218
+ const url = typeof rawUrl === "string" ? rawUrl.trim() : "";
1219
+ const v2 = detectEmbed(url);
1220
+ const safe = url ? safeHref(url) : null;
1221
+ let parsed = null;
1222
+ try {
1223
+ parsed = safe && /^https?:\/\//i.test(safe) ? new URL(safe) : null;
1224
+ } catch {
1225
+ parsed = null;
1226
+ }
1227
+ const { provider, label } = v2.provider === "link" && parsed ? detectMore(parsed) ?? v2 : v2;
1228
+ const link = { kind: "link", provider, label, url: safe };
1229
+ if (!parsed) return link;
1230
+ if (provider === "x") return { kind: "card", provider: "x", label, url: canonicalEmbedUrl(parsed.href) ?? parsed.href };
1231
+ if (provider === "audio") return parsed.protocol === "https:" ? { kind: "audio", provider: "audio", label, src: parsed.href } : link;
1232
+ const frame = frameOf(provider, parsed);
1233
+ if (!frame) return link;
1234
+ const height = frame.height ?? FULL_WIDTH_HEIGHT[provider];
1235
+ if (height) return { kind: "frame", provider, label, src: frame.src, width: "100%", height };
1236
+ const [width, h] = FRAME_SIZE[isEmbedAspect(aspect) ? aspect : "16:9"];
1237
+ return { kind: "frame", provider, label, src: frame.src, width, height: h };
1238
+ }
1239
+
803
1240
  // src/blocks/portableText.tsx
804
1241
  import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
805
1242
  var TOKEN = /^[a-z0-9-]+$/i;
@@ -931,8 +1368,119 @@ function Table({ value }) {
931
1368
  ] }) : notes ? /* @__PURE__ */ jsx3("p", { "data-notes": "", id: notesId, children: notes }) : null
932
1369
  ] }) : table;
933
1370
  }
934
- function buildComponents(renderBlock) {
935
- return {
1371
+ function CheckItem({ checked, address, children }) {
1372
+ return /* @__PURE__ */ jsxs2("li", { ...address, "data-checked": String(checked), children: [
1373
+ /* @__PURE__ */ jsx3("input", { type: "checkbox", disabled: true, defaultChecked: checked }),
1374
+ children
1375
+ ] });
1376
+ }
1377
+ function XCard({ url, card }) {
1378
+ return /* @__PURE__ */ jsxs2("blockquote", { className: "twitter-tweet bcms-embed-card", cite: url, children: [
1379
+ /* @__PURE__ */ jsx3("p", { children: card.text }),
1380
+ "\u2014 ",
1381
+ card.authorName,
1382
+ " ",
1383
+ /* @__PURE__ */ jsx3("a", { href: url, children: card.date || "View on X" })
1384
+ ] });
1385
+ }
1386
+ function RelatedPosts({ value, resolved }) {
1387
+ const { key, title, mode, count, picked, items } = relatedPostsView(value, resolved);
1388
+ if (!resolved) {
1389
+ return /* @__PURE__ */ jsx3("aside", { "data-rt": "related", "data-key": key, "data-mode": mode, "data-count": count, "data-posts": picked.join(","), "data-title": title || void 0 });
1390
+ }
1391
+ if (!items.length) return null;
1392
+ return /* @__PURE__ */ jsxs2("aside", { "data-rt": "related", "data-mode": mode, "data-count": count, "data-posts": picked.join(","), children: [
1393
+ title ? /* @__PURE__ */ jsx3("p", { "data-title": "", children: title }) : null,
1394
+ items.length ? /* @__PURE__ */ jsx3("ul", { children: items.map((p) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsx3("a", { href: p.url, children: p.title }) }, p.id)) }) : null
1395
+ ] });
1396
+ }
1397
+ function AuthorBio({ value, resolved }) {
1398
+ const { key, id, author } = authorBioView(value, resolved);
1399
+ if (!resolved) return /* @__PURE__ */ jsx3("aside", { "data-rt": "author", "data-key": key, "data-author": id });
1400
+ if (!author) return null;
1401
+ return /* @__PURE__ */ jsxs2("aside", { "data-rt": "author", "data-author": id, children: [
1402
+ author?.avatar ? /* @__PURE__ */ jsx3("img", { src: author.avatar, alt: "" }) : null,
1403
+ author ? /* @__PURE__ */ jsx3("p", { "data-name": "", children: author.name }) : null,
1404
+ author?.bio ? /* @__PURE__ */ jsx3("p", { "data-bio": "", children: author.bio }) : null
1405
+ ] });
1406
+ }
1407
+ function Columns({ value, components }) {
1408
+ const view = columnsView(value);
1409
+ if (!view) return null;
1410
+ const s = COLUMNS_STYLES[view.gap];
1411
+ return /* @__PURE__ */ jsx3("div", { "data-rt": "columns", "data-gap": view.gap, "data-cols": view.columns.length, style: styleObject(s.row), children: view.columns.map((c) => /* @__PURE__ */ jsx3("div", { "data-col": "true", style: styleObject(s.col), children: c.content.length ? /* @__PURE__ */ jsx3(PT, { value: c.content, components }) : null }, c.key)) });
1412
+ }
1413
+ function FootnoteMark({ value, footnotes }) {
1414
+ const note = typeof value.note === "string" ? value.note : "";
1415
+ const n = footnotes.nodes.get(value);
1416
+ if (!n) return /* @__PURE__ */ jsx3("sup", { "data-rt": "fn", "data-note": note });
1417
+ return /* @__PURE__ */ jsx3("sup", { "data-rt": "fn", id: `fnref-${n}`, "data-note": note, children: /* @__PURE__ */ jsx3("a", { href: `#fn-${n}`, "aria-label": `Footnote ${n}`, children: n }) });
1418
+ }
1419
+ function FootnoteList({ notes }) {
1420
+ if (!notes.length) return null;
1421
+ return /* @__PURE__ */ jsx3("ol", { className: "cs-footnotes", "data-rt": "footnotes", "aria-label": "Footnotes", children: notes.map((note, i) => /* @__PURE__ */ jsxs2("li", { id: `fn-${i + 1}`, children: [
1422
+ note,
1423
+ " ",
1424
+ /* @__PURE__ */ jsx3("a", { href: `#fnref-${i + 1}`, "aria-label": `Back to reference ${i + 1}`, children: "\u21A9" })
1425
+ ] }, i)) });
1426
+ }
1427
+ function Toc({ value, outline }) {
1428
+ const { title, label, depth } = tocView(value);
1429
+ const link = (o) => /* @__PURE__ */ jsx3("a", { href: `#${o.id}`, children: o.text });
1430
+ return /* @__PURE__ */ jsxs2("nav", { "data-rt": "toc", "data-depth": depth, "aria-label": label, children: [
1431
+ title ? /* @__PURE__ */ jsx3("p", { "data-title": "", children: title }) : null,
1432
+ /* @__PURE__ */ jsx3("ol", { children: tocTree(outline.items, depth).map((e) => /* @__PURE__ */ jsxs2("li", { children: [
1433
+ link(e),
1434
+ e.children.length ? /* @__PURE__ */ jsx3("ol", { children: e.children.map((c) => /* @__PURE__ */ jsx3("li", { children: link(c) }, c.id)) }) : null
1435
+ ] }, e.id)) })
1436
+ ] });
1437
+ }
1438
+ function Gallery({ value }) {
1439
+ const { layout, images } = galleryView(value);
1440
+ const s = GALLERY_STYLES[layout];
1441
+ return /* @__PURE__ */ jsx3("figure", { "data-rt": "gallery", "data-layout": layout, children: /* @__PURE__ */ jsx3("ul", { style: styleObject(s.ul), tabIndex: layout === "carousel" ? 0 : void 0, children: images.map((x) => /* @__PURE__ */ jsx3("li", { style: styleObject(s.li), children: /* @__PURE__ */ jsxs2("figure", { style: styleObject(s.figure), children: [
1442
+ /* @__PURE__ */ jsx3(
1443
+ "img",
1444
+ {
1445
+ alt: x.alt,
1446
+ style: styleObject(x.position ? `${s.img};object-position:${x.position}` : s.img),
1447
+ loading: "lazy",
1448
+ ...Object.fromEntries(x.data),
1449
+ src: x.src
1450
+ }
1451
+ ),
1452
+ x.caption ? /* @__PURE__ */ jsx3("figcaption", { children: x.caption }) : null
1453
+ ] }) }, x.key)) }) });
1454
+ }
1455
+ function Embed({ value, embeds }) {
1456
+ const v = value ?? {};
1457
+ const url = typeof v.url === "string" ? v.url.trim() : "";
1458
+ const aspect = isEmbedAspect(v.aspect) ? v.aspect : "16:9";
1459
+ const title = typeof v.title === "string" ? v.title.trim() : "";
1460
+ const r = resolveEmbed(url, aspect);
1461
+ const label = title || r.label;
1462
+ const card = r.kind === "card" ? embeds.cards?.[r.url] : void 0;
1463
+ let body = null;
1464
+ if (r.kind === "frame") {
1465
+ body = /* @__PURE__ */ jsx3("iframe", { src: r.src, title: label, width: r.width, height: r.height, loading: "lazy", referrerPolicy: "strict-origin-when-cross-origin", allowFullScreen: true });
1466
+ } else if (r.kind === "audio") {
1467
+ body = /* @__PURE__ */ jsx3("audio", { controls: true, preload: "none", src: r.src, title: label });
1468
+ } else if (r.kind === "card" && card) {
1469
+ body = /* @__PURE__ */ jsx3(XCard, { url: r.url, card });
1470
+ } else if (r.url) {
1471
+ body = /* @__PURE__ */ jsx3("a", { href: r.url, children: label });
1472
+ }
1473
+ return /* @__PURE__ */ jsx3("figure", { "data-rt": "embed", "data-url": url, "data-provider": r.provider, "data-aspect": aspect, "aria-label": title, children: body });
1474
+ }
1475
+ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes: /* @__PURE__ */ new WeakMap() }, footnotes = { notes: [], nodes: /* @__PURE__ */ new WeakMap() }, resolved) {
1476
+ const components = {
1477
+ // RT5: every top-level h2 and h3 carries its outline id, as html.ts writes it.
1478
+ block: {
1479
+ h2: ({ value, children }) => /* @__PURE__ */ jsx3("h2", { id: outline.nodes.get(value), children }),
1480
+ h3: ({ value, children }) => /* @__PURE__ */ jsx3("h3", { id: outline.nodes.get(value), children })
1481
+ },
1482
+ list: { check: ({ children }) => /* @__PURE__ */ jsx3("ul", { "data-task": "true", children }) },
1483
+ listItem: { check: ({ value, children }) => /* @__PURE__ */ jsx3(CheckItem, { checked: value.checked === true, children }) },
936
1484
  types: {
937
1485
  [PT_TYPES.divider]: () => /* @__PURE__ */ jsx3("hr", {}),
938
1486
  [PT_TYPES.table]: ({ value }) => /* @__PURE__ */ jsx3(Table, { value }),
@@ -953,6 +1501,7 @@ function buildComponents(renderBlock) {
953
1501
  if (rendered !== void 0) return /* @__PURE__ */ jsx3(Fragment, { children: rendered });
954
1502
  const builtIn = calloutOrToggle(value);
955
1503
  if (builtIn) return builtIn;
1504
+ if (columnsView(value)) return /* @__PURE__ */ jsx3(Columns, { value, components });
956
1505
  const copy = detachedComponentCopy(value);
957
1506
  if (copy) {
958
1507
  return /* @__PURE__ */ jsx3("div", { className: "bcms-block bcms-block--component", "data-bcms-component-copy": copy.componentId, "data-bcms-instance": copy.id, children: /* @__PURE__ */ jsx3(BlocksRenderer, { blocks: copy.blocks }) });
@@ -982,6 +1531,12 @@ function buildComponents(renderBlock) {
982
1531
  ] }) : null
983
1532
  ] });
984
1533
  },
1534
+ [PT_TYPES.embed]: ({ value }) => /* @__PURE__ */ jsx3(Embed, { value, embeds }),
1535
+ [PT_TYPES.gallery]: ({ value }) => /* @__PURE__ */ jsx3(Gallery, { value }),
1536
+ [PT_TYPES.tableOfContents]: ({ value }) => /* @__PURE__ */ jsx3(Toc, { value, outline }),
1537
+ [PT_TYPES.columns]: ({ value }) => /* @__PURE__ */ jsx3(Columns, { value, components }),
1538
+ [PT_TYPES.relatedPosts]: ({ value }) => /* @__PURE__ */ jsx3(RelatedPosts, { value, resolved }),
1539
+ [PT_TYPES.authorBio]: ({ value }) => /* @__PURE__ */ jsx3(AuthorBio, { value, resolved }),
985
1540
  /**
986
1541
  * Key takeaways — the React half of `portableTextToHtml`'s, and the same DOM: the V2 kit's
987
1542
  * `<aside data-rt="takeaways">`, read through the one `keyTakeawaysView` both renderers share.
@@ -994,6 +1549,7 @@ function buildComponents(renderBlock) {
994
1549
  /* @__PURE__ */ jsx3("ul", { children: points.map((p, i) => /* @__PURE__ */ jsx3("li", { children: p }, i)) })
995
1550
  ] });
996
1551
  },
1552
+ [PT_TYPES.footnote]: ({ value }) => /* @__PURE__ */ jsx3(FootnoteMark, { value, footnotes }),
997
1553
  [PT_TYPES.mention]: ({ value }) => {
998
1554
  const v = value;
999
1555
  return /* @__PURE__ */ jsx3("span", { className: "bcms-mention", children: typeof v.label === "string" ? v.label : "" });
@@ -1009,7 +1565,7 @@ function buildComponents(renderBlock) {
1009
1565
  const href = typeof v.href === "string" ? safeHref(v.href) : null;
1010
1566
  if (!href) return /* @__PURE__ */ jsx3(Fragment, { children });
1011
1567
  const blank = v.target === "_blank";
1012
- return /* @__PURE__ */ jsx3("a", { href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
1568
+ return /* @__PURE__ */ jsx3("a", { ...v.button === true ? { "data-button": "true" } : {}, href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
1013
1569
  },
1014
1570
  /**
1015
1571
  * The site's own inline markup, rendered as itself — the React half of
@@ -1058,6 +1614,7 @@ function buildComponents(renderBlock) {
1058
1614
  unknownType: ({ children }) => /* @__PURE__ */ jsx3(Fragment, { children: children ?? null }),
1059
1615
  unknownBlockStyle: ({ children }) => /* @__PURE__ */ jsx3("p", { children })
1060
1616
  };
1617
+ return components;
1061
1618
  }
1062
1619
  var isListItem = (node) => node._type === "block" && typeof node.listItem === "string" && !!node.listItem;
1063
1620
  function runsOf(blocks) {
@@ -1098,22 +1655,42 @@ function calloutOrToggle(value) {
1098
1655
  }
1099
1656
  return null;
1100
1657
  }
1101
- function PortableText({ value, bindingBase, renderBlock }) {
1658
+ function PortableText({ value, bindingBase, renderBlock, embeds, resolved }) {
1102
1659
  const blocks = React2.useMemo(() => normalizePortableText(readValue(value)), [value]);
1103
- const components = React2.useMemo(() => buildComponents(renderBlock), [renderBlock]);
1660
+ const outline = React2.useMemo(() => outlineOf(blocks), [blocks]);
1661
+ const footnotes = React2.useMemo(() => footnoteOrder(blocks), [blocks]);
1662
+ const components = React2.useMemo(
1663
+ () => buildComponents(renderBlock, embeds, outline, footnotes, resolved),
1664
+ [renderBlock, embeds, outline, footnotes, resolved]
1665
+ );
1104
1666
  const listComponents = React2.useMemo(() => {
1105
1667
  if (!bindingBase) return null;
1106
1668
  const keys = new Set(blocks.map((b) => b._key));
1107
1669
  return {
1108
1670
  ...components,
1109
- listItem: ({ value: item, children }) => keys.has(item._key) ? /* @__PURE__ */ jsx3("li", { "data-bcms-field": `${bindingBase}#${item._key}`, "data-bcms-kind": "richtext", children }) : /* @__PURE__ */ jsx3("li", { children })
1671
+ listItem: ({ value: item, children }) => {
1672
+ const address = keys.has(item._key) ? { "data-bcms-field": `${bindingBase}#${item._key}`, "data-bcms-kind": "richtext" } : void 0;
1673
+ if (item.listItem === "check") {
1674
+ return /* @__PURE__ */ jsx3(CheckItem, { checked: item.checked === true, address, children });
1675
+ }
1676
+ return /* @__PURE__ */ jsx3("li", { ...address, children });
1677
+ }
1110
1678
  };
1111
1679
  }, [bindingBase, blocks, components]);
1112
1680
  if (blocks.length === 0) return null;
1113
- if (!bindingBase || !listComponents) return /* @__PURE__ */ jsx3(PT, { value: blocks, components });
1114
- return /* @__PURE__ */ jsx3(Fragment, { children: runsOf(blocks).map(
1115
- (run) => isListItem(run[0]) ? /* @__PURE__ */ jsx3(PT, { value: run, components: listComponents }, run[0]._key) : /* @__PURE__ */ jsx3(Bound, { base: bindingBase, nodeKey: run[0]._key, children: /* @__PURE__ */ jsx3(PT, { value: run, components }) }, run[0]._key)
1116
- ) });
1681
+ const notes = /* @__PURE__ */ jsx3(FootnoteList, { notes: footnotes.notes });
1682
+ if (!bindingBase || !listComponents) {
1683
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
1684
+ /* @__PURE__ */ jsx3(PT, { value: blocks, components }),
1685
+ notes
1686
+ ] });
1687
+ }
1688
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
1689
+ runsOf(blocks).map(
1690
+ (run) => isListItem(run[0]) ? /* @__PURE__ */ jsx3(PT, { value: run, components: listComponents }, run[0]._key) : /* @__PURE__ */ jsx3(Bound, { base: bindingBase, nodeKey: run[0]._key, children: /* @__PURE__ */ jsx3(PT, { value: run, components }) }, run[0]._key)
1691
+ ),
1692
+ notes
1693
+ ] });
1117
1694
  }
1118
1695
  function readValue(value) {
1119
1696
  if (Array.isArray(value)) return value;
@@ -1287,7 +1864,7 @@ function Spacer({ height = 32, className = "" }) {
1287
1864
  // src/blocks/Columns.tsx
1288
1865
  import "react";
1289
1866
  import { jsx as jsx8 } from "react/jsx-runtime";
1290
- function Columns({
1867
+ function Columns2({
1291
1868
  columns,
1292
1869
  gap = 16,
1293
1870
  className = "",
@@ -1541,7 +2118,7 @@ var BLOCK_REGISTRY = {
1541
2118
  icon: "minus"
1542
2119
  },
1543
2120
  columns: {
1544
- component: Columns,
2121
+ component: Columns2,
1545
2122
  label: "Columns",
1546
2123
  icon: "columns"
1547
2124
  },