@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/blocks.d.ts +7 -1
- package/dist/blocks.js +384 -17
- package/dist/blocks.js.map +1 -1
- package/dist/index.js +384 -17
- package/dist/index.js.map +1 -1
- package/dist/registry.js +384 -17
- package/dist/registry.js.map +1 -1
- package/dist/renderer.js +384 -17
- package/dist/renderer.js.map +1 -1
- package/package.json +3 -3
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/
|
|
584
|
+
// ../richtext/src/gallery.ts
|
|
572
585
|
var isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
573
|
-
var
|
|
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:
|
|
626
|
+
id: isRecord4(value) && typeof value._key === "string" ? value._key : "",
|
|
579
627
|
componentId: typeof f.componentId === "string" ? f.componentId : "",
|
|
580
|
-
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(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
591
639
|
return t || null;
|
|
592
640
|
}
|
|
593
|
-
if (
|
|
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
|
|
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 (!
|
|
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:
|
|
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 (!
|
|
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 (!
|
|
770
|
+
if (!isRecord6(props)) return null;
|
|
682
771
|
const candidate = props.content ?? props.value;
|
|
683
|
-
const arr = Array.isArray(candidate) ? candidate :
|
|
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
|
|
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
|
|
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 }) =>
|
|
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;
|