@bettercms-ai/ui 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/registry.js CHANGED
@@ -183,6 +183,16 @@ 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",
186
196
  /**
187
197
  * A table. Spelled `table`, unprefixed, because that is what every other Portable Text
188
198
  * producer calls one — the Sanity-shaped `{_type:"table", rows}` this platform used to refuse
@@ -209,7 +219,10 @@ var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
209
219
  PT_TYPES.video,
210
220
  PT_TYPES.file,
211
221
  PT_TYPES.pullQuote,
212
- PT_TYPES.keyTakeaways
222
+ PT_TYPES.keyTakeaways,
223
+ PT_TYPES.embed,
224
+ PT_TYPES.gallery,
225
+ PT_TYPES.tableOfContents
213
226
  ]);
214
227
  var EMBEDDED_COMPONENT_KEY = "component";
215
228
 
@@ -684,16 +697,51 @@ function keyTakeawaysView(value) {
684
697
  return { title, points };
685
698
  }
686
699
 
687
- // ../richtext/src/component-copy.ts
700
+ // ../richtext/src/gallery.ts
688
701
  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;
702
+ var galleryMediaSrc = (raw) => safeHref(raw) ?? (raw !== "" && !raw.startsWith("//") && !/[:\s]/.test(raw) ? raw : null);
703
+ var GALLERY_STYLES = {
704
+ grid: {
705
+ ul: "display:grid;grid-template-columns:repeat(auto-fill,minmax(12rem,1fr));gap:1rem;list-style:none;margin:0;padding:0",
706
+ li: "margin:0",
707
+ figure: "margin:0",
708
+ img: "display:block;width:100%;height:auto"
709
+ },
710
+ carousel: {
711
+ ul: "display:flex;gap:1rem;overflow-x:auto;scroll-snap-type:x mandatory;list-style:none;margin:0;padding:0",
712
+ li: "flex:0 0 100%;scroll-snap-align:start;margin:0",
713
+ figure: "margin:0",
714
+ img: "display:block;width:100%;height:auto"
715
+ }
716
+ };
717
+ function galleryView(value) {
718
+ const v = isRecord3(value) ? value : {};
719
+ const layout = v.layout === "carousel" ? "carousel" : "grid";
720
+ const images = [];
721
+ (Array.isArray(v.images) ? v.images : []).forEach((item, i) => {
722
+ if (!isRecord3(item) || !isRecord3(item.image)) return;
723
+ const src = galleryMediaSrc(typeof item.image.src === "string" ? item.image.src.trim() : "");
724
+ if (!src) return;
725
+ images.push({
726
+ key: typeof item._key === "string" && item._key ? item._key : `i${i}`,
727
+ src,
728
+ alt: typeof item.image.alt === "string" ? item.image.alt : "",
729
+ caption: typeof item.caption === "string" ? item.caption.trim() : ""
730
+ });
731
+ });
732
+ return { layout, images };
733
+ }
734
+
735
+ // ../richtext/src/component-copy.ts
736
+ var isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
737
+ var fieldsOf = (value) => isRecord4(value) && value.schemaKey === EMBEDDED_COMPONENT_KEY && isRecord4(value.fields) ? value.fields : null;
690
738
  function detachedComponentCopy(value) {
691
739
  const f = fieldsOf(value);
692
740
  if (!f || f.detached !== true || !Array.isArray(f.blocks)) return null;
693
741
  return {
694
- id: isRecord3(value) && typeof value._key === "string" ? value._key : "",
742
+ id: isRecord4(value) && typeof value._key === "string" ? value._key : "",
695
743
  componentId: typeof f.componentId === "string" ? f.componentId : "",
696
- props: isRecord3(f.props) ? f.props : {},
744
+ props: isRecord4(f.props) ? f.props : {},
697
745
  blocks: f.blocks,
698
746
  ...typeof f.sourcePublishedAt === "string" ? { sourcePublishedAt: f.sourcePublishedAt } : {},
699
747
  ...typeof f.variant === "string" ? { variant: f.variant } : {}
@@ -706,7 +754,7 @@ function componentCopyLines(copy) {
706
754
  const t = v.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
707
755
  return t || null;
708
756
  }
709
- if (isRecord3(v)) return line(v.label ?? v.text ?? v.title);
757
+ if (isRecord4(v)) return line(v.label ?? v.text ?? v.title);
710
758
  return null;
711
759
  };
712
760
  return Object.values(copy.props).map(line).filter((s) => !!s);
@@ -733,6 +781,10 @@ function portableTextToPlainText(value) {
733
781
  lines.push(...title ? [title] : [], ...points);
734
782
  continue;
735
783
  }
784
+ if (node._type === PT_TYPES.gallery) {
785
+ lines.push(...galleryView(node).images.map((x) => x.caption).filter(Boolean));
786
+ continue;
787
+ }
736
788
  const table = migrateTable(node);
737
789
  if (table) {
738
790
  lines.push(...tableLines(table));
@@ -771,20 +823,57 @@ var pullQuoteLines = (v) => {
771
823
  return [text(v.quote), ...by ? [`\u2014 ${by}`] : []].filter(Boolean);
772
824
  };
773
825
 
826
+ // ../richtext/src/toc.ts
827
+ import { slugify } from "@bettercms-ai/types";
828
+ var isRecord5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
829
+ var headingText = (node) => (Array.isArray(node.children) ? node.children : []).map((c) => isRecord5(c) && typeof c.text === "string" ? c.text : "").join("").replace(/​/g, "").trim();
830
+ function outlineOf(value) {
831
+ const items = [];
832
+ const nodes = /* @__PURE__ */ new WeakMap();
833
+ const used = /* @__PURE__ */ new Set();
834
+ for (const node of Array.isArray(value) ? value : []) {
835
+ if (!isRecord5(node) || node._type !== "block" || node.listItem || node.style !== "h2" && node.style !== "h3") continue;
836
+ const text = headingText(node);
837
+ if (!text) continue;
838
+ const base = slugify(text);
839
+ let id = base;
840
+ for (let n = 2; used.has(id); n++) id = `${base}-${n}`;
841
+ used.add(id);
842
+ nodes.set(node, id);
843
+ items.push({ id, text, depth: node.style === "h2" ? 2 : 3 });
844
+ }
845
+ return { items, nodes };
846
+ }
847
+ function tocTree(items, depth) {
848
+ const out = [];
849
+ for (const it of items) {
850
+ if (it.depth === 3 && depth === "2") continue;
851
+ const last = out[out.length - 1];
852
+ if (it.depth === 3 && last) last.children.push(it);
853
+ else out.push({ ...it, children: [] });
854
+ }
855
+ return out;
856
+ }
857
+ function tocView(value) {
858
+ const v = isRecord5(value) ? value : {};
859
+ const title = typeof v.title === "string" ? v.title.trim() : "";
860
+ return { title, label: title || "Table of contents", depth: v.depth === "2" ? "2" : "3" };
861
+ }
862
+
774
863
  // ../richtext/src/read.ts
775
- var isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
864
+ var isRecord6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
776
865
  function normalizePortableText(value, key = randomKey) {
777
866
  if (!Array.isArray(value)) return [];
778
867
  return value.flatMap((raw) => {
779
- if (!isRecord4(raw) || typeof raw._type !== "string") return [];
868
+ if (!isRecord6(raw) || typeof raw._type !== "string") return [];
780
869
  const table = migrateTable(raw, key, (v, cellKey) => normalizePortableText(v, cellKey));
781
870
  if (table) return [table];
782
- const node = { ...raw, _key: isRecord4(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
871
+ const node = { ...raw, _key: isRecord6(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
783
872
  if (node._type === "block") {
784
873
  if (!Array.isArray(node.markDefs)) node.markDefs = [];
785
874
  if (typeof node.style !== "string") node.style = "normal";
786
875
  node.children = (Array.isArray(node.children) ? node.children : []).map((c) => {
787
- if (!isRecord4(c)) return c;
876
+ if (!isRecord6(c)) return c;
788
877
  const child = { ...c, _key: typeof c._key === "string" && c._key ? c._key : key() };
789
878
  if (child._type === "span" && !Array.isArray(child.marks)) child.marks = [];
790
879
  return child;
@@ -794,12 +883,219 @@ function normalizePortableText(value, key = randomKey) {
794
883
  });
795
884
  }
796
885
  function readStructuredContent(props) {
797
- if (!isRecord4(props)) return null;
886
+ if (!isRecord6(props)) return null;
798
887
  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;
888
+ const arr = Array.isArray(candidate) ? candidate : isRecord6(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
800
889
  return arr && arr.length > 0 ? arr : null;
801
890
  }
802
891
 
892
+ // ../richtext/src/video.ts
893
+ var videoId = (value) => value && /^[a-zA-Z0-9_-]{6,32}$/.test(value) ? value : null;
894
+ function resolveVideoSource(value) {
895
+ if (typeof value !== "string" || !value.trim()) return null;
896
+ let parsed;
897
+ try {
898
+ parsed = new URL(value.trim());
899
+ } catch {
900
+ return null;
901
+ }
902
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
903
+ const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
904
+ let id = null;
905
+ if (host === "youtu.be") id = videoId(parsed.pathname.split("/").filter(Boolean)[0]);
906
+ if (host === "youtube.com" || host === "m.youtube.com" || host === "youtube-nocookie.com") {
907
+ id = videoId(
908
+ parsed.searchParams.get("v") ?? parsed.pathname.match(/^\/(?:embed|shorts)\/([^/?#]+)/)?.[1]
909
+ );
910
+ }
911
+ if (id) {
912
+ return {
913
+ kind: "iframe",
914
+ src: `https://www.youtube-nocookie.com/embed/${id}`,
915
+ provider: "YouTube"
916
+ };
917
+ }
918
+ if (host === "vimeo.com" || host === "player.vimeo.com") {
919
+ const segments = parsed.pathname.split("/").filter(Boolean);
920
+ let vimeoId = null;
921
+ for (let i = segments.length - 1; i >= 0; i -= 1) {
922
+ if (/^\d+$/.test(segments[i])) {
923
+ vimeoId = segments[i];
924
+ break;
925
+ }
926
+ }
927
+ if (vimeoId) {
928
+ return {
929
+ kind: "iframe",
930
+ src: `https://player.vimeo.com/video/${vimeoId}`,
931
+ provider: "Vimeo"
932
+ };
933
+ }
934
+ }
935
+ if (/\.(?:m4v|mp4|og[gv]|webm)$/i.test(parsed.pathname)) {
936
+ return { kind: "video", src: parsed.toString(), provider: "Video file" };
937
+ }
938
+ return null;
939
+ }
940
+
941
+ // ../richtext/src/embed.ts
942
+ var EMBED_ASPECTS = ["16:9", "4:3", "1:1", "9:16"];
943
+ var isEmbedAspect = (v) => EMBED_ASPECTS.includes(v);
944
+ function detectEmbed(url) {
945
+ const u = url.trim();
946
+ let host = "";
947
+ try {
948
+ host = new URL(u).hostname.replace(/^www\./, "");
949
+ } catch {
950
+ host = "";
951
+ }
952
+ const is = (re) => re.test(u);
953
+ if (is(/(?:youtube\.com\/(?:watch\?|embed\/|shorts\/|live\/)|youtu\.be\/)/i)) return { provider: "youtube", label: "YouTube", host };
954
+ if (is(/vimeo\.com\/(?:video\/)?\d+/i)) return { provider: "vimeo", label: "Vimeo", host };
955
+ if (is(/(?:twitter|x)\.com\/[^/]+\/status\/\d+/i)) return { provider: "x", label: "X post", host };
956
+ if (is(/codepen\.io\/[^/]+\/(?:pen|embed)\//i)) return { provider: "codepen", label: "CodePen", host };
957
+ if (is(/figma\.com\/(?:file|design|proto|board)\//i)) return { provider: "figma", label: "Figma", host };
958
+ if (is(/(?:google\.[a-z.]+\/maps|maps\.app\.goo\.gl|goo\.gl\/maps)/i)) return { provider: "maps", label: "Google Maps", host };
959
+ if (is(/open\.spotify\.com\/(?:episode|track|show|playlist|album)\//i)) return { provider: "spotify", label: "Spotify", host };
960
+ if (is(/podcasts\.apple\.com\//i)) return { provider: "podcast", label: "Apple Podcasts", host };
961
+ if (is(/\.(?:mp3|m4a|wav|ogg|aac)(?:\?|$)/i)) return { provider: "audio", label: "Audio", host };
962
+ return { provider: "link", label: host || "Link", host };
963
+ }
964
+ function detectMore(url) {
965
+ const host = url.hostname.toLowerCase().replace(/^www\./, "");
966
+ const path = url.pathname;
967
+ if (host === "youtube-nocookie.com" && path.startsWith("/embed/")) return { provider: "youtube", label: "YouTube" };
968
+ if (host === "open.spotify.com" && /^\/embed\/(?:episode|track|show|playlist|album)\//.test(path)) return { provider: "spotify", label: "Spotify" };
969
+ if (host === "calendly.com") return { provider: "calendly", label: "Calendly" };
970
+ if (host.endsWith(".typeform.com") && /^\/to\//.test(path)) return { provider: "typeform", label: "Typeform" };
971
+ if (host === "loom.com" && /^\/(?:share|embed)\//.test(path)) return { provider: "loom", label: "Loom" };
972
+ if (host === "airtable.com") return { provider: "airtable", label: "Airtable" };
973
+ if (host === "docs.google.com") {
974
+ const kind = path.split("/")[1];
975
+ const label = { forms: "Google Forms", document: "Google Docs", spreadsheets: "Google Sheets", presentation: "Google Slides" }[kind ?? ""];
976
+ if (label) return { provider: "google", label };
977
+ }
978
+ return null;
979
+ }
980
+ var FRAME_SIZE = {
981
+ "16:9": ["640", "360"],
982
+ "4:3": ["640", "480"],
983
+ "1:1": ["640", "640"],
984
+ "9:16": ["360", "640"]
985
+ };
986
+ var ID = /^[A-Za-z0-9_-]{1,64}$/;
987
+ var FULL_WIDTH_HEIGHT = { calendly: "700", typeform: "500", airtable: "533" };
988
+ function frameOf(provider, url) {
989
+ const parts = url.pathname.split("/").filter(Boolean);
990
+ switch (provider) {
991
+ case "youtube":
992
+ case "vimeo": {
993
+ const video = resolveVideoSource(url.href);
994
+ return video?.kind === "iframe" ? { src: video.src } : null;
995
+ }
996
+ case "codepen": {
997
+ const [user, mode, id] = parts;
998
+ return user && ID.test(user) && (mode === "pen" || mode === "embed") && id && ID.test(id) ? { src: `https://codepen.io/${user}/embed/${id}?default-tab=result` } : null;
999
+ }
1000
+ case "figma": {
1001
+ const [mode, key] = parts;
1002
+ if (!mode || !["file", "design", "proto", "board"].includes(mode) || !key || !ID.test(key)) return null;
1003
+ const frame = new URL(`https://embed.figma.com/${mode}/${key}`);
1004
+ const node = url.searchParams.get("node-id");
1005
+ if (node && /^[0-9A-Za-z:_-]{1,64}$/.test(node)) frame.searchParams.set("node-id", node);
1006
+ frame.searchParams.set("embed-host", "bettercms");
1007
+ return { src: frame.href };
1008
+ }
1009
+ case "maps": {
1010
+ if (!/^(?:www\.)?google\.[a-z.]+$/i.test(url.hostname)) return null;
1011
+ const pb = url.searchParams.get("pb");
1012
+ if (parts[0] === "maps" && parts[1] === "embed" && pb && /^[!A-Za-z0-9._:%+-]{1,2000}$/.test(pb)) {
1013
+ return { src: `https://www.google.com/maps/embed?pb=${encodeURIComponent(pb)}` };
1014
+ }
1015
+ const at = /@(-?\d{1,3}(?:\.\d+)?),(-?\d{1,3}(?:\.\d+)?)/.exec(url.pathname);
1016
+ const place = parts[0] === "maps" && parts[1] === "place" && parts[2] ? decodeURIComponent(parts[2]).replace(/\+/g, " ") : null;
1017
+ const q = at ? `${at[1]},${at[2]}` : url.searchParams.get("q") ?? place;
1018
+ if (!q || q.length > 200) return null;
1019
+ return { src: `https://maps.google.com/maps?q=${encodeURIComponent(q)}&output=embed` };
1020
+ }
1021
+ case "spotify": {
1022
+ const [type, id] = parts[0] === "embed" ? parts.slice(1) : parts;
1023
+ if (!type || !["episode", "track", "show", "playlist", "album"].includes(type) || !id || !ID.test(id)) return null;
1024
+ return { src: `https://open.spotify.com/embed/${type}/${id}`, height: type === "track" || type === "episode" ? "152" : "352" };
1025
+ }
1026
+ case "podcast":
1027
+ 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;
1028
+ case "calendly":
1029
+ 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;
1030
+ case "typeform": {
1031
+ const id = parts[0] === "to" ? parts[1] : void 0;
1032
+ return id && /^[A-Za-z0-9]{4,32}$/.test(id) ? { src: `https://form.typeform.com/to/${id}` } : null;
1033
+ }
1034
+ case "loom": {
1035
+ const id = parts[1];
1036
+ return (parts[0] === "share" || parts[0] === "embed") && id && /^[0-9a-f]{32}$/.test(id) ? { src: `https://www.loom.com/embed/${id}` } : null;
1037
+ }
1038
+ case "airtable": {
1039
+ const ids = parts[0] === "embed" ? parts.slice(1) : parts;
1040
+ 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;
1041
+ }
1042
+ case "google": {
1043
+ const [kind, d, maybeE, maybeId] = parts;
1044
+ if (d !== "d") return null;
1045
+ const published = maybeE === "e";
1046
+ const id = published ? maybeId : maybeE;
1047
+ if (!id || !/^[A-Za-z0-9_-]{10,128}$/.test(id)) return null;
1048
+ const base = `https://docs.google.com/${kind}/d/${published ? `e/${id}` : id}`;
1049
+ if (kind === "forms") return { src: `${base}/viewform?embedded=true`, height: "800" };
1050
+ if (kind === "document") return { src: published ? `${base}/pub?embedded=true` : `${base}/preview`, height: "600" };
1051
+ if (kind === "spreadsheets") return { src: published ? `${base}/pubhtml?widget=true&headers=false` : `${base}/preview`, height: "600" };
1052
+ if (kind === "presentation") return { src: published ? `${base}/embed` : `${base}/preview` };
1053
+ return null;
1054
+ }
1055
+ default:
1056
+ return null;
1057
+ }
1058
+ }
1059
+ var X_STATUS = /^\/([A-Za-z0-9_]{1,15})\/status\/(\d{1,25})(?:\/|$)/;
1060
+ var X_HOSTS = /^(?:www\.|mobile\.)?(?:twitter|x)\.com$/i;
1061
+ function canonicalEmbedUrl(raw) {
1062
+ const url = typeof raw === "string" ? raw.trim() : "";
1063
+ const safe = url ? safeHref(url) : null;
1064
+ if (!safe || !/^https?:\/\//i.test(safe)) return null;
1065
+ let parsed;
1066
+ try {
1067
+ parsed = new URL(safe);
1068
+ } catch {
1069
+ return null;
1070
+ }
1071
+ const status = X_HOSTS.test(parsed.hostname) ? X_STATUS.exec(parsed.pathname) : null;
1072
+ if (status) return `https://x.com/${status[1].toLowerCase()}/status/${status[2]}`;
1073
+ parsed.hash = "";
1074
+ return parsed.href;
1075
+ }
1076
+ function resolveEmbed(rawUrl, aspect = "16:9") {
1077
+ const url = typeof rawUrl === "string" ? rawUrl.trim() : "";
1078
+ const v2 = detectEmbed(url);
1079
+ const safe = url ? safeHref(url) : null;
1080
+ let parsed = null;
1081
+ try {
1082
+ parsed = safe && /^https?:\/\//i.test(safe) ? new URL(safe) : null;
1083
+ } catch {
1084
+ parsed = null;
1085
+ }
1086
+ const { provider, label } = v2.provider === "link" && parsed ? detectMore(parsed) ?? v2 : v2;
1087
+ const link = { kind: "link", provider, label, url: safe };
1088
+ if (!parsed) return link;
1089
+ if (provider === "x") return { kind: "card", provider: "x", label, url: canonicalEmbedUrl(parsed.href) ?? parsed.href };
1090
+ if (provider === "audio") return parsed.protocol === "https:" ? { kind: "audio", provider: "audio", label, src: parsed.href } : link;
1091
+ const frame = frameOf(provider, parsed);
1092
+ if (!frame) return link;
1093
+ const height = frame.height ?? FULL_WIDTH_HEIGHT[provider];
1094
+ if (height) return { kind: "frame", provider, label, src: frame.src, width: "100%", height };
1095
+ const [width, h] = FRAME_SIZE[isEmbedAspect(aspect) ? aspect : "16:9"];
1096
+ return { kind: "frame", provider, label, src: frame.src, width, height: h };
1097
+ }
1098
+
803
1099
  // src/blocks/portableText.tsx
804
1100
  import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
805
1101
  var TOKEN = /^[a-z0-9-]+$/i;
@@ -931,8 +1227,69 @@ function Table({ value }) {
931
1227
  ] }) : notes ? /* @__PURE__ */ jsx3("p", { "data-notes": "", id: notesId, children: notes }) : null
932
1228
  ] }) : table;
933
1229
  }
934
- function buildComponents(renderBlock) {
1230
+ function CheckItem({ checked, address, children }) {
1231
+ return /* @__PURE__ */ jsxs2("li", { ...address, "data-checked": String(checked), children: [
1232
+ /* @__PURE__ */ jsx3("input", { type: "checkbox", disabled: true, defaultChecked: checked }),
1233
+ children
1234
+ ] });
1235
+ }
1236
+ function XCard({ url, card }) {
1237
+ return /* @__PURE__ */ jsxs2("blockquote", { className: "twitter-tweet bcms-embed-card", cite: url, children: [
1238
+ /* @__PURE__ */ jsx3("p", { children: card.text }),
1239
+ "\u2014 ",
1240
+ card.authorName,
1241
+ " ",
1242
+ /* @__PURE__ */ jsx3("a", { href: url, children: card.date || "View on X" })
1243
+ ] });
1244
+ }
1245
+ function Toc({ value, outline }) {
1246
+ const { title, label, depth } = tocView(value);
1247
+ const link = (o) => /* @__PURE__ */ jsx3("a", { href: `#${o.id}`, children: o.text });
1248
+ return /* @__PURE__ */ jsxs2("nav", { "data-rt": "toc", "data-depth": depth, "aria-label": label, children: [
1249
+ title ? /* @__PURE__ */ jsx3("p", { "data-title": "", children: title }) : null,
1250
+ /* @__PURE__ */ jsx3("ol", { children: tocTree(outline.items, depth).map((e) => /* @__PURE__ */ jsxs2("li", { children: [
1251
+ link(e),
1252
+ e.children.length ? /* @__PURE__ */ jsx3("ol", { children: e.children.map((c) => /* @__PURE__ */ jsx3("li", { children: link(c) }, c.id)) }) : null
1253
+ ] }, e.id)) })
1254
+ ] });
1255
+ }
1256
+ function Gallery({ value }) {
1257
+ const { layout, images } = galleryView(value);
1258
+ const s = GALLERY_STYLES[layout];
1259
+ 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: [
1260
+ /* @__PURE__ */ jsx3("img", { src: x.src, alt: x.alt, style: styleObject(s.img), loading: "lazy" }),
1261
+ x.caption ? /* @__PURE__ */ jsx3("figcaption", { children: x.caption }) : null
1262
+ ] }) }, x.key)) }) });
1263
+ }
1264
+ function Embed({ value, embeds }) {
1265
+ const v = value ?? {};
1266
+ const url = typeof v.url === "string" ? v.url.trim() : "";
1267
+ const aspect = isEmbedAspect(v.aspect) ? v.aspect : "16:9";
1268
+ const title = typeof v.title === "string" ? v.title.trim() : "";
1269
+ const r = resolveEmbed(url, aspect);
1270
+ const label = title || r.label;
1271
+ const card = r.kind === "card" ? embeds.cards?.[r.url] : void 0;
1272
+ let body = null;
1273
+ if (r.kind === "frame") {
1274
+ 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 });
1275
+ } else if (r.kind === "audio") {
1276
+ body = /* @__PURE__ */ jsx3("audio", { controls: true, preload: "none", src: r.src, title: label });
1277
+ } else if (r.kind === "card" && card) {
1278
+ body = /* @__PURE__ */ jsx3(XCard, { url: r.url, card });
1279
+ } else if (r.url) {
1280
+ body = /* @__PURE__ */ jsx3("a", { href: r.url, children: label });
1281
+ }
1282
+ return /* @__PURE__ */ jsx3("figure", { "data-rt": "embed", "data-url": url, "data-provider": r.provider, "data-aspect": aspect, "aria-label": title, children: body });
1283
+ }
1284
+ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes: /* @__PURE__ */ new WeakMap() }) {
935
1285
  return {
1286
+ // RT5: every top-level h2 and h3 carries its outline id, as html.ts writes it.
1287
+ block: {
1288
+ h2: ({ value, children }) => /* @__PURE__ */ jsx3("h2", { id: outline.nodes.get(value), children }),
1289
+ h3: ({ value, children }) => /* @__PURE__ */ jsx3("h3", { id: outline.nodes.get(value), children })
1290
+ },
1291
+ list: { check: ({ children }) => /* @__PURE__ */ jsx3("ul", { "data-task": "true", children }) },
1292
+ listItem: { check: ({ value, children }) => /* @__PURE__ */ jsx3(CheckItem, { checked: value.checked === true, children }) },
936
1293
  types: {
937
1294
  [PT_TYPES.divider]: () => /* @__PURE__ */ jsx3("hr", {}),
938
1295
  [PT_TYPES.table]: ({ value }) => /* @__PURE__ */ jsx3(Table, { value }),
@@ -982,6 +1339,9 @@ function buildComponents(renderBlock) {
982
1339
  ] }) : null
983
1340
  ] });
984
1341
  },
1342
+ [PT_TYPES.embed]: ({ value }) => /* @__PURE__ */ jsx3(Embed, { value, embeds }),
1343
+ [PT_TYPES.gallery]: ({ value }) => /* @__PURE__ */ jsx3(Gallery, { value }),
1344
+ [PT_TYPES.tableOfContents]: ({ value }) => /* @__PURE__ */ jsx3(Toc, { value, outline }),
985
1345
  /**
986
1346
  * Key takeaways — the React half of `portableTextToHtml`'s, and the same DOM: the V2 kit's
987
1347
  * `<aside data-rt="takeaways">`, read through the one `keyTakeawaysView` both renderers share.
@@ -1009,7 +1369,7 @@ function buildComponents(renderBlock) {
1009
1369
  const href = typeof v.href === "string" ? safeHref(v.href) : null;
1010
1370
  if (!href) return /* @__PURE__ */ jsx3(Fragment, { children });
1011
1371
  const blank = v.target === "_blank";
1012
- return /* @__PURE__ */ jsx3("a", { href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
1372
+ return /* @__PURE__ */ jsx3("a", { ...v.button === true ? { "data-button": "true" } : {}, href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
1013
1373
  },
1014
1374
  /**
1015
1375
  * The site's own inline markup, rendered as itself — the React half of
@@ -1098,15 +1458,22 @@ function calloutOrToggle(value) {
1098
1458
  }
1099
1459
  return null;
1100
1460
  }
1101
- function PortableText({ value, bindingBase, renderBlock }) {
1461
+ function PortableText({ value, bindingBase, renderBlock, embeds }) {
1102
1462
  const blocks = React2.useMemo(() => normalizePortableText(readValue(value)), [value]);
1103
- const components = React2.useMemo(() => buildComponents(renderBlock), [renderBlock]);
1463
+ const outline = React2.useMemo(() => outlineOf(blocks), [blocks]);
1464
+ const components = React2.useMemo(() => buildComponents(renderBlock, embeds, outline), [renderBlock, embeds, outline]);
1104
1465
  const listComponents = React2.useMemo(() => {
1105
1466
  if (!bindingBase) return null;
1106
1467
  const keys = new Set(blocks.map((b) => b._key));
1107
1468
  return {
1108
1469
  ...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 })
1470
+ listItem: ({ value: item, children }) => {
1471
+ const address = keys.has(item._key) ? { "data-bcms-field": `${bindingBase}#${item._key}`, "data-bcms-kind": "richtext" } : void 0;
1472
+ if (item.listItem === "check") {
1473
+ return /* @__PURE__ */ jsx3(CheckItem, { checked: item.checked === true, address, children });
1474
+ }
1475
+ return /* @__PURE__ */ jsx3("li", { ...address, children });
1476
+ }
1110
1477
  };
1111
1478
  }, [bindingBase, blocks, components]);
1112
1479
  if (blocks.length === 0) return null;