@bettercms-ai/ui 0.20.1 → 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/renderer.js CHANGED
@@ -67,6 +67,16 @@ var PT_TYPES = {
67
67
  pullQuote: "pullQuote",
68
68
  /** Key takeaways — see `PortableTextKeyTakeaways`. The V2 kit's `<aside data-rt="takeaways">`. */
69
69
  keyTakeaways: "keyTakeaways",
70
+ /**
71
+ * A third-party embed (X, CodePen, Figma, Maps, Spotify, Podcasts, audio, a link card) — see
72
+ * `PortableTextEmbed` and `embed.ts`. The V2 kit's `<figure data-rt="embed">`. Gated by the
73
+ * `embed` feature; YouTube and Vimeo links still resolve here too, through the video resolver.
74
+ */
75
+ embed: "embed",
76
+ /** A gallery of images, a grid or a carousel — see `PortableTextGallery`. The V2 kit's `<figure data-rt="gallery">`. */
77
+ gallery: "gallery",
78
+ /** A table of contents built from the headings — see `PortableTextTableOfContents`. The V2 kit's `<nav data-rt="toc">`. */
79
+ tableOfContents: "tableOfContents",
70
80
  /**
71
81
  * A table. Spelled `table`, unprefixed, because that is what every other Portable Text
72
82
  * producer calls one — the Sanity-shaped `{_type:"table", rows}` this platform used to refuse
@@ -93,7 +103,10 @@ var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
93
103
  PT_TYPES.video,
94
104
  PT_TYPES.file,
95
105
  PT_TYPES.pullQuote,
96
- PT_TYPES.keyTakeaways
106
+ PT_TYPES.keyTakeaways,
107
+ PT_TYPES.embed,
108
+ PT_TYPES.gallery,
109
+ PT_TYPES.tableOfContents
97
110
  ]);
98
111
  var EMBEDDED_COMPONENT_KEY = "component";
99
112
 
@@ -568,16 +581,51 @@ function keyTakeawaysView(value) {
568
581
  return { title, points };
569
582
  }
570
583
 
571
- // ../richtext/src/component-copy.ts
584
+ // ../richtext/src/gallery.ts
572
585
  var isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
573
- var fieldsOf = (value) => isRecord3(value) && value.schemaKey === EMBEDDED_COMPONENT_KEY && isRecord3(value.fields) ? value.fields : null;
586
+ var galleryMediaSrc = (raw) => safeHref(raw) ?? (raw !== "" && !raw.startsWith("//") && !/[:\s]/.test(raw) ? raw : null);
587
+ var GALLERY_STYLES = {
588
+ grid: {
589
+ ul: "display:grid;grid-template-columns:repeat(auto-fill,minmax(12rem,1fr));gap:1rem;list-style:none;margin:0;padding:0",
590
+ li: "margin:0",
591
+ figure: "margin:0",
592
+ img: "display:block;width:100%;height:auto"
593
+ },
594
+ carousel: {
595
+ ul: "display:flex;gap:1rem;overflow-x:auto;scroll-snap-type:x mandatory;list-style:none;margin:0;padding:0",
596
+ li: "flex:0 0 100%;scroll-snap-align:start;margin:0",
597
+ figure: "margin:0",
598
+ img: "display:block;width:100%;height:auto"
599
+ }
600
+ };
601
+ function galleryView(value) {
602
+ const v = isRecord3(value) ? value : {};
603
+ const layout = v.layout === "carousel" ? "carousel" : "grid";
604
+ const images = [];
605
+ (Array.isArray(v.images) ? v.images : []).forEach((item, i) => {
606
+ if (!isRecord3(item) || !isRecord3(item.image)) return;
607
+ const src = galleryMediaSrc(typeof item.image.src === "string" ? item.image.src.trim() : "");
608
+ if (!src) return;
609
+ images.push({
610
+ key: typeof item._key === "string" && item._key ? item._key : `i${i}`,
611
+ src,
612
+ alt: typeof item.image.alt === "string" ? item.image.alt : "",
613
+ caption: typeof item.caption === "string" ? item.caption.trim() : ""
614
+ });
615
+ });
616
+ return { layout, images };
617
+ }
618
+
619
+ // ../richtext/src/component-copy.ts
620
+ 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;
574
622
  function detachedComponentCopy(value) {
575
623
  const f = fieldsOf(value);
576
624
  if (!f || f.detached !== true || !Array.isArray(f.blocks)) return null;
577
625
  return {
578
- id: isRecord3(value) && typeof value._key === "string" ? value._key : "",
626
+ id: isRecord4(value) && typeof value._key === "string" ? value._key : "",
579
627
  componentId: typeof f.componentId === "string" ? f.componentId : "",
580
- props: isRecord3(f.props) ? f.props : {},
628
+ props: isRecord4(f.props) ? f.props : {},
581
629
  blocks: f.blocks,
582
630
  ...typeof f.sourcePublishedAt === "string" ? { sourcePublishedAt: f.sourcePublishedAt } : {},
583
631
  ...typeof f.variant === "string" ? { variant: f.variant } : {}
@@ -590,7 +638,7 @@ function componentCopyLines(copy) {
590
638
  const t = v.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
591
639
  return t || null;
592
640
  }
593
- if (isRecord3(v)) return line(v.label ?? v.text ?? v.title);
641
+ if (isRecord4(v)) return line(v.label ?? v.text ?? v.title);
594
642
  return null;
595
643
  };
596
644
  return Object.values(copy.props).map(line).filter((s) => !!s);
@@ -617,6 +665,10 @@ function portableTextToPlainText(value) {
617
665
  lines.push(...title ? [title] : [], ...points);
618
666
  continue;
619
667
  }
668
+ if (node._type === PT_TYPES.gallery) {
669
+ lines.push(...galleryView(node).images.map((x) => x.caption).filter(Boolean));
670
+ continue;
671
+ }
620
672
  const table = migrateTable(node);
621
673
  if (table) {
622
674
  lines.push(...tableLines(table));
@@ -655,20 +707,57 @@ var pullQuoteLines = (v) => {
655
707
  return [text(v.quote), ...by ? [`\u2014 ${by}`] : []].filter(Boolean);
656
708
  };
657
709
 
710
+ // ../richtext/src/toc.ts
711
+ 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();
714
+ function outlineOf(value) {
715
+ const items = [];
716
+ const nodes = /* @__PURE__ */ new WeakMap();
717
+ const used = /* @__PURE__ */ new Set();
718
+ for (const node of Array.isArray(value) ? value : []) {
719
+ if (!isRecord5(node) || node._type !== "block" || node.listItem || node.style !== "h2" && node.style !== "h3") continue;
720
+ const text = headingText(node);
721
+ 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);
726
+ nodes.set(node, id);
727
+ items.push({ id, text, depth: node.style === "h2" ? 2 : 3 });
728
+ }
729
+ return { items, nodes };
730
+ }
731
+ function tocTree(items, depth) {
732
+ const out = [];
733
+ for (const it of items) {
734
+ if (it.depth === 3 && depth === "2") continue;
735
+ const last = out[out.length - 1];
736
+ if (it.depth === 3 && last) last.children.push(it);
737
+ else out.push({ ...it, children: [] });
738
+ }
739
+ return out;
740
+ }
741
+ function tocView(value) {
742
+ const v = isRecord5(value) ? value : {};
743
+ const title = typeof v.title === "string" ? v.title.trim() : "";
744
+ return { title, label: title || "Table of contents", depth: v.depth === "2" ? "2" : "3" };
745
+ }
746
+
658
747
  // ../richtext/src/read.ts
659
- var isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
748
+ var isRecord6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
660
749
  function normalizePortableText(value, key = randomKey) {
661
750
  if (!Array.isArray(value)) return [];
662
751
  return value.flatMap((raw) => {
663
- if (!isRecord4(raw) || typeof raw._type !== "string") return [];
752
+ if (!isRecord6(raw) || typeof raw._type !== "string") return [];
664
753
  const table = migrateTable(raw, key, (v, cellKey) => normalizePortableText(v, cellKey));
665
754
  if (table) return [table];
666
- const node = { ...raw, _key: isRecord4(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
755
+ const node = { ...raw, _key: isRecord6(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
667
756
  if (node._type === "block") {
668
757
  if (!Array.isArray(node.markDefs)) node.markDefs = [];
669
758
  if (typeof node.style !== "string") node.style = "normal";
670
759
  node.children = (Array.isArray(node.children) ? node.children : []).map((c) => {
671
- if (!isRecord4(c)) return c;
760
+ if (!isRecord6(c)) return c;
672
761
  const child = { ...c, _key: typeof c._key === "string" && c._key ? c._key : key() };
673
762
  if (child._type === "span" && !Array.isArray(child.marks)) child.marks = [];
674
763
  return child;
@@ -678,12 +767,219 @@ function normalizePortableText(value, key = randomKey) {
678
767
  });
679
768
  }
680
769
  function readStructuredContent(props) {
681
- if (!isRecord4(props)) return null;
770
+ if (!isRecord6(props)) return null;
682
771
  const candidate = props.content ?? props.value;
683
- const arr = Array.isArray(candidate) ? candidate : isRecord4(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
772
+ const arr = Array.isArray(candidate) ? candidate : isRecord6(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
684
773
  return arr && arr.length > 0 ? arr : null;
685
774
  }
686
775
 
776
+ // ../richtext/src/video.ts
777
+ var videoId = (value) => value && /^[a-zA-Z0-9_-]{6,32}$/.test(value) ? value : null;
778
+ function resolveVideoSource(value) {
779
+ if (typeof value !== "string" || !value.trim()) return null;
780
+ let parsed;
781
+ try {
782
+ parsed = new URL(value.trim());
783
+ } catch {
784
+ return null;
785
+ }
786
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
787
+ const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
788
+ let id = null;
789
+ if (host === "youtu.be") id = videoId(parsed.pathname.split("/").filter(Boolean)[0]);
790
+ if (host === "youtube.com" || host === "m.youtube.com" || host === "youtube-nocookie.com") {
791
+ id = videoId(
792
+ parsed.searchParams.get("v") ?? parsed.pathname.match(/^\/(?:embed|shorts)\/([^/?#]+)/)?.[1]
793
+ );
794
+ }
795
+ if (id) {
796
+ return {
797
+ kind: "iframe",
798
+ src: `https://www.youtube-nocookie.com/embed/${id}`,
799
+ provider: "YouTube"
800
+ };
801
+ }
802
+ if (host === "vimeo.com" || host === "player.vimeo.com") {
803
+ const segments = parsed.pathname.split("/").filter(Boolean);
804
+ let vimeoId = null;
805
+ for (let i = segments.length - 1; i >= 0; i -= 1) {
806
+ if (/^\d+$/.test(segments[i])) {
807
+ vimeoId = segments[i];
808
+ break;
809
+ }
810
+ }
811
+ if (vimeoId) {
812
+ return {
813
+ kind: "iframe",
814
+ src: `https://player.vimeo.com/video/${vimeoId}`,
815
+ provider: "Vimeo"
816
+ };
817
+ }
818
+ }
819
+ if (/\.(?:m4v|mp4|og[gv]|webm)$/i.test(parsed.pathname)) {
820
+ return { kind: "video", src: parsed.toString(), provider: "Video file" };
821
+ }
822
+ return null;
823
+ }
824
+
825
+ // ../richtext/src/embed.ts
826
+ var EMBED_ASPECTS = ["16:9", "4:3", "1:1", "9:16"];
827
+ var isEmbedAspect = (v) => EMBED_ASPECTS.includes(v);
828
+ function detectEmbed(url) {
829
+ const u = url.trim();
830
+ let host = "";
831
+ try {
832
+ host = new URL(u).hostname.replace(/^www\./, "");
833
+ } catch {
834
+ host = "";
835
+ }
836
+ const is = (re) => re.test(u);
837
+ if (is(/(?:youtube\.com\/(?:watch\?|embed\/|shorts\/|live\/)|youtu\.be\/)/i)) return { provider: "youtube", label: "YouTube", host };
838
+ if (is(/vimeo\.com\/(?:video\/)?\d+/i)) return { provider: "vimeo", label: "Vimeo", host };
839
+ if (is(/(?:twitter|x)\.com\/[^/]+\/status\/\d+/i)) return { provider: "x", label: "X post", host };
840
+ if (is(/codepen\.io\/[^/]+\/(?:pen|embed)\//i)) return { provider: "codepen", label: "CodePen", host };
841
+ if (is(/figma\.com\/(?:file|design|proto|board)\//i)) return { provider: "figma", label: "Figma", host };
842
+ if (is(/(?:google\.[a-z.]+\/maps|maps\.app\.goo\.gl|goo\.gl\/maps)/i)) return { provider: "maps", label: "Google Maps", host };
843
+ if (is(/open\.spotify\.com\/(?:episode|track|show|playlist|album)\//i)) return { provider: "spotify", label: "Spotify", host };
844
+ if (is(/podcasts\.apple\.com\//i)) return { provider: "podcast", label: "Apple Podcasts", host };
845
+ if (is(/\.(?:mp3|m4a|wav|ogg|aac)(?:\?|$)/i)) return { provider: "audio", label: "Audio", host };
846
+ return { provider: "link", label: host || "Link", host };
847
+ }
848
+ function detectMore(url) {
849
+ const host = url.hostname.toLowerCase().replace(/^www\./, "");
850
+ const path = url.pathname;
851
+ if (host === "youtube-nocookie.com" && path.startsWith("/embed/")) return { provider: "youtube", label: "YouTube" };
852
+ if (host === "open.spotify.com" && /^\/embed\/(?:episode|track|show|playlist|album)\//.test(path)) return { provider: "spotify", label: "Spotify" };
853
+ if (host === "calendly.com") return { provider: "calendly", label: "Calendly" };
854
+ if (host.endsWith(".typeform.com") && /^\/to\//.test(path)) return { provider: "typeform", label: "Typeform" };
855
+ if (host === "loom.com" && /^\/(?:share|embed)\//.test(path)) return { provider: "loom", label: "Loom" };
856
+ if (host === "airtable.com") return { provider: "airtable", label: "Airtable" };
857
+ if (host === "docs.google.com") {
858
+ const kind = path.split("/")[1];
859
+ const label = { forms: "Google Forms", document: "Google Docs", spreadsheets: "Google Sheets", presentation: "Google Slides" }[kind ?? ""];
860
+ if (label) return { provider: "google", label };
861
+ }
862
+ return null;
863
+ }
864
+ var FRAME_SIZE = {
865
+ "16:9": ["640", "360"],
866
+ "4:3": ["640", "480"],
867
+ "1:1": ["640", "640"],
868
+ "9:16": ["360", "640"]
869
+ };
870
+ var ID = /^[A-Za-z0-9_-]{1,64}$/;
871
+ var FULL_WIDTH_HEIGHT = { calendly: "700", typeform: "500", airtable: "533" };
872
+ function frameOf(provider, url) {
873
+ const parts = url.pathname.split("/").filter(Boolean);
874
+ switch (provider) {
875
+ case "youtube":
876
+ case "vimeo": {
877
+ const video = resolveVideoSource(url.href);
878
+ return video?.kind === "iframe" ? { src: video.src } : null;
879
+ }
880
+ case "codepen": {
881
+ const [user, mode, id] = parts;
882
+ return user && ID.test(user) && (mode === "pen" || mode === "embed") && id && ID.test(id) ? { src: `https://codepen.io/${user}/embed/${id}?default-tab=result` } : null;
883
+ }
884
+ case "figma": {
885
+ const [mode, key] = parts;
886
+ if (!mode || !["file", "design", "proto", "board"].includes(mode) || !key || !ID.test(key)) return null;
887
+ const frame = new URL(`https://embed.figma.com/${mode}/${key}`);
888
+ const node = url.searchParams.get("node-id");
889
+ if (node && /^[0-9A-Za-z:_-]{1,64}$/.test(node)) frame.searchParams.set("node-id", node);
890
+ frame.searchParams.set("embed-host", "bettercms");
891
+ return { src: frame.href };
892
+ }
893
+ case "maps": {
894
+ if (!/^(?:www\.)?google\.[a-z.]+$/i.test(url.hostname)) return null;
895
+ const pb = url.searchParams.get("pb");
896
+ if (parts[0] === "maps" && parts[1] === "embed" && pb && /^[!A-Za-z0-9._:%+-]{1,2000}$/.test(pb)) {
897
+ return { src: `https://www.google.com/maps/embed?pb=${encodeURIComponent(pb)}` };
898
+ }
899
+ const at = /@(-?\d{1,3}(?:\.\d+)?),(-?\d{1,3}(?:\.\d+)?)/.exec(url.pathname);
900
+ const place = parts[0] === "maps" && parts[1] === "place" && parts[2] ? decodeURIComponent(parts[2]).replace(/\+/g, " ") : null;
901
+ const q = at ? `${at[1]},${at[2]}` : url.searchParams.get("q") ?? place;
902
+ if (!q || q.length > 200) return null;
903
+ return { src: `https://maps.google.com/maps?q=${encodeURIComponent(q)}&output=embed` };
904
+ }
905
+ case "spotify": {
906
+ const [type, id] = parts[0] === "embed" ? parts.slice(1) : parts;
907
+ if (!type || !["episode", "track", "show", "playlist", "album"].includes(type) || !id || !ID.test(id)) return null;
908
+ return { src: `https://open.spotify.com/embed/${type}/${id}`, height: type === "track" || type === "episode" ? "152" : "352" };
909
+ }
910
+ case "podcast":
911
+ 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;
912
+ case "calendly":
913
+ 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;
914
+ case "typeform": {
915
+ const id = parts[0] === "to" ? parts[1] : void 0;
916
+ return id && /^[A-Za-z0-9]{4,32}$/.test(id) ? { src: `https://form.typeform.com/to/${id}` } : null;
917
+ }
918
+ case "loom": {
919
+ const id = parts[1];
920
+ return (parts[0] === "share" || parts[0] === "embed") && id && /^[0-9a-f]{32}$/.test(id) ? { src: `https://www.loom.com/embed/${id}` } : null;
921
+ }
922
+ case "airtable": {
923
+ const ids = parts[0] === "embed" ? parts.slice(1) : parts;
924
+ 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;
925
+ }
926
+ case "google": {
927
+ const [kind, d, maybeE, maybeId] = parts;
928
+ if (d !== "d") return null;
929
+ const published = maybeE === "e";
930
+ const id = published ? maybeId : maybeE;
931
+ if (!id || !/^[A-Za-z0-9_-]{10,128}$/.test(id)) return null;
932
+ const base = `https://docs.google.com/${kind}/d/${published ? `e/${id}` : id}`;
933
+ if (kind === "forms") return { src: `${base}/viewform?embedded=true`, height: "800" };
934
+ if (kind === "document") return { src: published ? `${base}/pub?embedded=true` : `${base}/preview`, height: "600" };
935
+ if (kind === "spreadsheets") return { src: published ? `${base}/pubhtml?widget=true&headers=false` : `${base}/preview`, height: "600" };
936
+ if (kind === "presentation") return { src: published ? `${base}/embed` : `${base}/preview` };
937
+ return null;
938
+ }
939
+ default:
940
+ return null;
941
+ }
942
+ }
943
+ var X_STATUS = /^\/([A-Za-z0-9_]{1,15})\/status\/(\d{1,25})(?:\/|$)/;
944
+ var X_HOSTS = /^(?:www\.|mobile\.)?(?:twitter|x)\.com$/i;
945
+ function canonicalEmbedUrl(raw) {
946
+ const url = typeof raw === "string" ? raw.trim() : "";
947
+ const safe = url ? safeHref(url) : null;
948
+ if (!safe || !/^https?:\/\//i.test(safe)) return null;
949
+ let parsed;
950
+ try {
951
+ parsed = new URL(safe);
952
+ } catch {
953
+ return null;
954
+ }
955
+ const status = X_HOSTS.test(parsed.hostname) ? X_STATUS.exec(parsed.pathname) : null;
956
+ if (status) return `https://x.com/${status[1].toLowerCase()}/status/${status[2]}`;
957
+ parsed.hash = "";
958
+ return parsed.href;
959
+ }
960
+ function resolveEmbed(rawUrl, aspect = "16:9") {
961
+ const url = typeof rawUrl === "string" ? rawUrl.trim() : "";
962
+ const v2 = detectEmbed(url);
963
+ const safe = url ? safeHref(url) : null;
964
+ let parsed = null;
965
+ try {
966
+ parsed = safe && /^https?:\/\//i.test(safe) ? new URL(safe) : null;
967
+ } catch {
968
+ parsed = null;
969
+ }
970
+ const { provider, label } = v2.provider === "link" && parsed ? detectMore(parsed) ?? v2 : v2;
971
+ const link = { kind: "link", provider, label, url: safe };
972
+ if (!parsed) return link;
973
+ if (provider === "x") return { kind: "card", provider: "x", label, url: canonicalEmbedUrl(parsed.href) ?? parsed.href };
974
+ if (provider === "audio") return parsed.protocol === "https:" ? { kind: "audio", provider: "audio", label, src: parsed.href } : link;
975
+ const frame = frameOf(provider, parsed);
976
+ if (!frame) return link;
977
+ const height = frame.height ?? FULL_WIDTH_HEIGHT[provider];
978
+ if (height) return { kind: "frame", provider, label, src: frame.src, width: "100%", height };
979
+ const [width, h] = FRAME_SIZE[isEmbedAspect(aspect) ? aspect : "16:9"];
980
+ return { kind: "frame", provider, label, src: frame.src, width, height: h };
981
+ }
982
+
687
983
  // src/blocks/portableText.tsx
688
984
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
689
985
  var TOKEN = /^[a-z0-9-]+$/i;
@@ -815,8 +1111,69 @@ function Table({ value }) {
815
1111
  ] }) : notes ? /* @__PURE__ */ jsx2("p", { "data-notes": "", id: notesId, children: notes }) : null
816
1112
  ] }) : table;
817
1113
  }
818
- function buildComponents(renderBlock) {
1114
+ function CheckItem({ checked, address, children }) {
1115
+ return /* @__PURE__ */ jsxs("li", { ...address, "data-checked": String(checked), children: [
1116
+ /* @__PURE__ */ jsx2("input", { type: "checkbox", disabled: true, defaultChecked: checked }),
1117
+ children
1118
+ ] });
1119
+ }
1120
+ function XCard({ url, card }) {
1121
+ return /* @__PURE__ */ jsxs("blockquote", { className: "twitter-tweet bcms-embed-card", cite: url, children: [
1122
+ /* @__PURE__ */ jsx2("p", { children: card.text }),
1123
+ "\u2014 ",
1124
+ card.authorName,
1125
+ " ",
1126
+ /* @__PURE__ */ jsx2("a", { href: url, children: card.date || "View on X" })
1127
+ ] });
1128
+ }
1129
+ function Toc({ value, outline }) {
1130
+ const { title, label, depth } = tocView(value);
1131
+ const link = (o) => /* @__PURE__ */ jsx2("a", { href: `#${o.id}`, children: o.text });
1132
+ return /* @__PURE__ */ jsxs("nav", { "data-rt": "toc", "data-depth": depth, "aria-label": label, children: [
1133
+ title ? /* @__PURE__ */ jsx2("p", { "data-title": "", children: title }) : null,
1134
+ /* @__PURE__ */ jsx2("ol", { children: tocTree(outline.items, depth).map((e) => /* @__PURE__ */ jsxs("li", { children: [
1135
+ link(e),
1136
+ e.children.length ? /* @__PURE__ */ jsx2("ol", { children: e.children.map((c) => /* @__PURE__ */ jsx2("li", { children: link(c) }, c.id)) }) : null
1137
+ ] }, e.id)) })
1138
+ ] });
1139
+ }
1140
+ function Gallery({ value }) {
1141
+ const { layout, images } = galleryView(value);
1142
+ const s = GALLERY_STYLES[layout];
1143
+ 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" }),
1145
+ x.caption ? /* @__PURE__ */ jsx2("figcaption", { children: x.caption }) : null
1146
+ ] }) }, x.key)) }) });
1147
+ }
1148
+ function Embed({ value, embeds }) {
1149
+ const v = value ?? {};
1150
+ const url = typeof v.url === "string" ? v.url.trim() : "";
1151
+ const aspect = isEmbedAspect(v.aspect) ? v.aspect : "16:9";
1152
+ const title = typeof v.title === "string" ? v.title.trim() : "";
1153
+ const r = resolveEmbed(url, aspect);
1154
+ const label = title || r.label;
1155
+ const card = r.kind === "card" ? embeds.cards?.[r.url] : void 0;
1156
+ let body = null;
1157
+ if (r.kind === "frame") {
1158
+ body = /* @__PURE__ */ jsx2("iframe", { src: r.src, title: label, width: r.width, height: r.height, loading: "lazy", referrerPolicy: "strict-origin-when-cross-origin", allowFullScreen: true });
1159
+ } else if (r.kind === "audio") {
1160
+ body = /* @__PURE__ */ jsx2("audio", { controls: true, preload: "none", src: r.src, title: label });
1161
+ } else if (r.kind === "card" && card) {
1162
+ body = /* @__PURE__ */ jsx2(XCard, { url: r.url, card });
1163
+ } else if (r.url) {
1164
+ body = /* @__PURE__ */ jsx2("a", { href: r.url, children: label });
1165
+ }
1166
+ return /* @__PURE__ */ jsx2("figure", { "data-rt": "embed", "data-url": url, "data-provider": r.provider, "data-aspect": aspect, "aria-label": title, children: body });
1167
+ }
1168
+ function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes: /* @__PURE__ */ new WeakMap() }) {
819
1169
  return {
1170
+ // RT5: every top-level h2 and h3 carries its outline id, as html.ts writes it.
1171
+ block: {
1172
+ h2: ({ value, children }) => /* @__PURE__ */ jsx2("h2", { id: outline.nodes.get(value), children }),
1173
+ h3: ({ value, children }) => /* @__PURE__ */ jsx2("h3", { id: outline.nodes.get(value), children })
1174
+ },
1175
+ list: { check: ({ children }) => /* @__PURE__ */ jsx2("ul", { "data-task": "true", children }) },
1176
+ listItem: { check: ({ value, children }) => /* @__PURE__ */ jsx2(CheckItem, { checked: value.checked === true, children }) },
820
1177
  types: {
821
1178
  [PT_TYPES.divider]: () => /* @__PURE__ */ jsx2("hr", {}),
822
1179
  [PT_TYPES.table]: ({ value }) => /* @__PURE__ */ jsx2(Table, { value }),
@@ -866,6 +1223,9 @@ function buildComponents(renderBlock) {
866
1223
  ] }) : null
867
1224
  ] });
868
1225
  },
1226
+ [PT_TYPES.embed]: ({ value }) => /* @__PURE__ */ jsx2(Embed, { value, embeds }),
1227
+ [PT_TYPES.gallery]: ({ value }) => /* @__PURE__ */ jsx2(Gallery, { value }),
1228
+ [PT_TYPES.tableOfContents]: ({ value }) => /* @__PURE__ */ jsx2(Toc, { value, outline }),
869
1229
  /**
870
1230
  * Key takeaways — the React half of `portableTextToHtml`'s, and the same DOM: the V2 kit's
871
1231
  * `<aside data-rt="takeaways">`, read through the one `keyTakeawaysView` both renderers share.
@@ -893,7 +1253,7 @@ function buildComponents(renderBlock) {
893
1253
  const href = typeof v.href === "string" ? safeHref(v.href) : null;
894
1254
  if (!href) return /* @__PURE__ */ jsx2(Fragment, { children });
895
1255
  const blank = v.target === "_blank";
896
- return /* @__PURE__ */ jsx2("a", { href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
1256
+ return /* @__PURE__ */ jsx2("a", { ...v.button === true ? { "data-button": "true" } : {}, href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
897
1257
  },
898
1258
  /**
899
1259
  * The site's own inline markup, rendered as itself — the React half of
@@ -982,15 +1342,22 @@ function calloutOrToggle(value) {
982
1342
  }
983
1343
  return null;
984
1344
  }
985
- function PortableText({ value, bindingBase, renderBlock }) {
1345
+ function PortableText({ value, bindingBase, renderBlock, embeds }) {
986
1346
  const blocks = React.useMemo(() => normalizePortableText(readValue(value)), [value]);
987
- const components = React.useMemo(() => buildComponents(renderBlock), [renderBlock]);
1347
+ const outline = React.useMemo(() => outlineOf(blocks), [blocks]);
1348
+ const components = React.useMemo(() => buildComponents(renderBlock, embeds, outline), [renderBlock, embeds, outline]);
988
1349
  const listComponents = React.useMemo(() => {
989
1350
  if (!bindingBase) return null;
990
1351
  const keys = new Set(blocks.map((b) => b._key));
991
1352
  return {
992
1353
  ...components,
993
- listItem: ({ value: item, children }) => keys.has(item._key) ? /* @__PURE__ */ jsx2("li", { "data-bcms-field": `${bindingBase}#${item._key}`, "data-bcms-kind": "richtext", children }) : /* @__PURE__ */ jsx2("li", { children })
1354
+ listItem: ({ value: item, children }) => {
1355
+ const address = keys.has(item._key) ? { "data-bcms-field": `${bindingBase}#${item._key}`, "data-bcms-kind": "richtext" } : void 0;
1356
+ if (item.listItem === "check") {
1357
+ return /* @__PURE__ */ jsx2(CheckItem, { checked: item.checked === true, address, children });
1358
+ }
1359
+ return /* @__PURE__ */ jsx2("li", { ...address, children });
1360
+ }
994
1361
  };
995
1362
  }, [bindingBase, blocks, components]);
996
1363
  if (blocks.length === 0) return null;