@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/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/
|
|
700
|
+
// ../richtext/src/gallery.ts
|
|
688
701
|
var isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
689
|
-
var
|
|
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:
|
|
742
|
+
id: isRecord4(value) && typeof value._key === "string" ? value._key : "",
|
|
695
743
|
componentId: typeof f.componentId === "string" ? f.componentId : "",
|
|
696
|
-
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(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
707
755
|
return t || null;
|
|
708
756
|
}
|
|
709
|
-
if (
|
|
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
|
|
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 (!
|
|
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:
|
|
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 (!
|
|
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 (!
|
|
886
|
+
if (!isRecord6(props)) return null;
|
|
798
887
|
const candidate = props.content ?? props.value;
|
|
799
|
-
const arr = Array.isArray(candidate) ? candidate :
|
|
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
|
|
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
|
|
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 }) =>
|
|
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;
|