@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/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/index.js
CHANGED
|
@@ -616,6 +616,16 @@ var PT_TYPES = {
|
|
|
616
616
|
pullQuote: "pullQuote",
|
|
617
617
|
/** Key takeaways — see `PortableTextKeyTakeaways`. The V2 kit's `<aside data-rt="takeaways">`. */
|
|
618
618
|
keyTakeaways: "keyTakeaways",
|
|
619
|
+
/**
|
|
620
|
+
* A third-party embed (X, CodePen, Figma, Maps, Spotify, Podcasts, audio, a link card) — see
|
|
621
|
+
* `PortableTextEmbed` and `embed.ts`. The V2 kit's `<figure data-rt="embed">`. Gated by the
|
|
622
|
+
* `embed` feature; YouTube and Vimeo links still resolve here too, through the video resolver.
|
|
623
|
+
*/
|
|
624
|
+
embed: "embed",
|
|
625
|
+
/** A gallery of images, a grid or a carousel — see `PortableTextGallery`. The V2 kit's `<figure data-rt="gallery">`. */
|
|
626
|
+
gallery: "gallery",
|
|
627
|
+
/** A table of contents built from the headings — see `PortableTextTableOfContents`. The V2 kit's `<nav data-rt="toc">`. */
|
|
628
|
+
tableOfContents: "tableOfContents",
|
|
619
629
|
/**
|
|
620
630
|
* A table. Spelled `table`, unprefixed, because that is what every other Portable Text
|
|
621
631
|
* producer calls one — the Sanity-shaped `{_type:"table", rows}` this platform used to refuse
|
|
@@ -642,7 +652,10 @@ var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
|
|
|
642
652
|
PT_TYPES.video,
|
|
643
653
|
PT_TYPES.file,
|
|
644
654
|
PT_TYPES.pullQuote,
|
|
645
|
-
PT_TYPES.keyTakeaways
|
|
655
|
+
PT_TYPES.keyTakeaways,
|
|
656
|
+
PT_TYPES.embed,
|
|
657
|
+
PT_TYPES.gallery,
|
|
658
|
+
PT_TYPES.tableOfContents
|
|
646
659
|
]);
|
|
647
660
|
var EMBEDDED_COMPONENT_KEY = "component";
|
|
648
661
|
|
|
@@ -1117,16 +1130,51 @@ function keyTakeawaysView(value) {
|
|
|
1117
1130
|
return { title, points };
|
|
1118
1131
|
}
|
|
1119
1132
|
|
|
1120
|
-
// ../richtext/src/
|
|
1133
|
+
// ../richtext/src/gallery.ts
|
|
1121
1134
|
var isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1122
|
-
var
|
|
1135
|
+
var galleryMediaSrc = (raw) => safeHref(raw) ?? (raw !== "" && !raw.startsWith("//") && !/[:\s]/.test(raw) ? raw : null);
|
|
1136
|
+
var GALLERY_STYLES = {
|
|
1137
|
+
grid: {
|
|
1138
|
+
ul: "display:grid;grid-template-columns:repeat(auto-fill,minmax(12rem,1fr));gap:1rem;list-style:none;margin:0;padding:0",
|
|
1139
|
+
li: "margin:0",
|
|
1140
|
+
figure: "margin:0",
|
|
1141
|
+
img: "display:block;width:100%;height:auto"
|
|
1142
|
+
},
|
|
1143
|
+
carousel: {
|
|
1144
|
+
ul: "display:flex;gap:1rem;overflow-x:auto;scroll-snap-type:x mandatory;list-style:none;margin:0;padding:0",
|
|
1145
|
+
li: "flex:0 0 100%;scroll-snap-align:start;margin:0",
|
|
1146
|
+
figure: "margin:0",
|
|
1147
|
+
img: "display:block;width:100%;height:auto"
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
function galleryView(value) {
|
|
1151
|
+
const v = isRecord3(value) ? value : {};
|
|
1152
|
+
const layout = v.layout === "carousel" ? "carousel" : "grid";
|
|
1153
|
+
const images = [];
|
|
1154
|
+
(Array.isArray(v.images) ? v.images : []).forEach((item, i) => {
|
|
1155
|
+
if (!isRecord3(item) || !isRecord3(item.image)) return;
|
|
1156
|
+
const src = galleryMediaSrc(typeof item.image.src === "string" ? item.image.src.trim() : "");
|
|
1157
|
+
if (!src) return;
|
|
1158
|
+
images.push({
|
|
1159
|
+
key: typeof item._key === "string" && item._key ? item._key : `i${i}`,
|
|
1160
|
+
src,
|
|
1161
|
+
alt: typeof item.image.alt === "string" ? item.image.alt : "",
|
|
1162
|
+
caption: typeof item.caption === "string" ? item.caption.trim() : ""
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
return { layout, images };
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// ../richtext/src/component-copy.ts
|
|
1169
|
+
var isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1170
|
+
var fieldsOf = (value) => isRecord4(value) && value.schemaKey === EMBEDDED_COMPONENT_KEY && isRecord4(value.fields) ? value.fields : null;
|
|
1123
1171
|
function detachedComponentCopy(value) {
|
|
1124
1172
|
const f = fieldsOf(value);
|
|
1125
1173
|
if (!f || f.detached !== true || !Array.isArray(f.blocks)) return null;
|
|
1126
1174
|
return {
|
|
1127
|
-
id:
|
|
1175
|
+
id: isRecord4(value) && typeof value._key === "string" ? value._key : "",
|
|
1128
1176
|
componentId: typeof f.componentId === "string" ? f.componentId : "",
|
|
1129
|
-
props:
|
|
1177
|
+
props: isRecord4(f.props) ? f.props : {},
|
|
1130
1178
|
blocks: f.blocks,
|
|
1131
1179
|
...typeof f.sourcePublishedAt === "string" ? { sourcePublishedAt: f.sourcePublishedAt } : {},
|
|
1132
1180
|
...typeof f.variant === "string" ? { variant: f.variant } : {}
|
|
@@ -1139,7 +1187,7 @@ function componentCopyLines(copy) {
|
|
|
1139
1187
|
const t = v.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
1140
1188
|
return t || null;
|
|
1141
1189
|
}
|
|
1142
|
-
if (
|
|
1190
|
+
if (isRecord4(v)) return line(v.label ?? v.text ?? v.title);
|
|
1143
1191
|
return null;
|
|
1144
1192
|
};
|
|
1145
1193
|
return Object.values(copy.props).map(line).filter((s) => !!s);
|
|
@@ -1166,6 +1214,10 @@ function portableTextToPlainText(value) {
|
|
|
1166
1214
|
lines.push(...title ? [title] : [], ...points);
|
|
1167
1215
|
continue;
|
|
1168
1216
|
}
|
|
1217
|
+
if (node._type === PT_TYPES.gallery) {
|
|
1218
|
+
lines.push(...galleryView(node).images.map((x) => x.caption).filter(Boolean));
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1169
1221
|
const table = migrateTable(node);
|
|
1170
1222
|
if (table) {
|
|
1171
1223
|
lines.push(...tableLines(table));
|
|
@@ -1204,20 +1256,57 @@ var pullQuoteLines = (v) => {
|
|
|
1204
1256
|
return [text(v.quote), ...by ? [`\u2014 ${by}`] : []].filter(Boolean);
|
|
1205
1257
|
};
|
|
1206
1258
|
|
|
1259
|
+
// ../richtext/src/toc.ts
|
|
1260
|
+
import { slugify } from "@bettercms-ai/types";
|
|
1261
|
+
var isRecord5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1262
|
+
var headingText = (node) => (Array.isArray(node.children) ? node.children : []).map((c) => isRecord5(c) && typeof c.text === "string" ? c.text : "").join("").replace(//g, "").trim();
|
|
1263
|
+
function outlineOf(value) {
|
|
1264
|
+
const items = [];
|
|
1265
|
+
const nodes = /* @__PURE__ */ new WeakMap();
|
|
1266
|
+
const used = /* @__PURE__ */ new Set();
|
|
1267
|
+
for (const node of Array.isArray(value) ? value : []) {
|
|
1268
|
+
if (!isRecord5(node) || node._type !== "block" || node.listItem || node.style !== "h2" && node.style !== "h3") continue;
|
|
1269
|
+
const text = headingText(node);
|
|
1270
|
+
if (!text) continue;
|
|
1271
|
+
const base = slugify(text);
|
|
1272
|
+
let id = base;
|
|
1273
|
+
for (let n = 2; used.has(id); n++) id = `${base}-${n}`;
|
|
1274
|
+
used.add(id);
|
|
1275
|
+
nodes.set(node, id);
|
|
1276
|
+
items.push({ id, text, depth: node.style === "h2" ? 2 : 3 });
|
|
1277
|
+
}
|
|
1278
|
+
return { items, nodes };
|
|
1279
|
+
}
|
|
1280
|
+
function tocTree(items, depth) {
|
|
1281
|
+
const out = [];
|
|
1282
|
+
for (const it of items) {
|
|
1283
|
+
if (it.depth === 3 && depth === "2") continue;
|
|
1284
|
+
const last = out[out.length - 1];
|
|
1285
|
+
if (it.depth === 3 && last) last.children.push(it);
|
|
1286
|
+
else out.push({ ...it, children: [] });
|
|
1287
|
+
}
|
|
1288
|
+
return out;
|
|
1289
|
+
}
|
|
1290
|
+
function tocView(value) {
|
|
1291
|
+
const v = isRecord5(value) ? value : {};
|
|
1292
|
+
const title = typeof v.title === "string" ? v.title.trim() : "";
|
|
1293
|
+
return { title, label: title || "Table of contents", depth: v.depth === "2" ? "2" : "3" };
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1207
1296
|
// ../richtext/src/read.ts
|
|
1208
|
-
var
|
|
1297
|
+
var isRecord6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1209
1298
|
function normalizePortableText(value, key = randomKey) {
|
|
1210
1299
|
if (!Array.isArray(value)) return [];
|
|
1211
1300
|
return value.flatMap((raw) => {
|
|
1212
|
-
if (!
|
|
1301
|
+
if (!isRecord6(raw) || typeof raw._type !== "string") return [];
|
|
1213
1302
|
const table = migrateTable(raw, key, (v, cellKey) => normalizePortableText(v, cellKey));
|
|
1214
1303
|
if (table) return [table];
|
|
1215
|
-
const node = { ...raw, _key:
|
|
1304
|
+
const node = { ...raw, _key: isRecord6(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
|
|
1216
1305
|
if (node._type === "block") {
|
|
1217
1306
|
if (!Array.isArray(node.markDefs)) node.markDefs = [];
|
|
1218
1307
|
if (typeof node.style !== "string") node.style = "normal";
|
|
1219
1308
|
node.children = (Array.isArray(node.children) ? node.children : []).map((c) => {
|
|
1220
|
-
if (!
|
|
1309
|
+
if (!isRecord6(c)) return c;
|
|
1221
1310
|
const child = { ...c, _key: typeof c._key === "string" && c._key ? c._key : key() };
|
|
1222
1311
|
if (child._type === "span" && !Array.isArray(child.marks)) child.marks = [];
|
|
1223
1312
|
return child;
|
|
@@ -1227,12 +1316,219 @@ function normalizePortableText(value, key = randomKey) {
|
|
|
1227
1316
|
});
|
|
1228
1317
|
}
|
|
1229
1318
|
function readStructuredContent(props) {
|
|
1230
|
-
if (!
|
|
1319
|
+
if (!isRecord6(props)) return null;
|
|
1231
1320
|
const candidate = props.content ?? props.value;
|
|
1232
|
-
const arr = Array.isArray(candidate) ? candidate :
|
|
1321
|
+
const arr = Array.isArray(candidate) ? candidate : isRecord6(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
|
|
1233
1322
|
return arr && arr.length > 0 ? arr : null;
|
|
1234
1323
|
}
|
|
1235
1324
|
|
|
1325
|
+
// ../richtext/src/video.ts
|
|
1326
|
+
var videoId = (value) => value && /^[a-zA-Z0-9_-]{6,32}$/.test(value) ? value : null;
|
|
1327
|
+
function resolveVideoSource(value) {
|
|
1328
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
1329
|
+
let parsed;
|
|
1330
|
+
try {
|
|
1331
|
+
parsed = new URL(value.trim());
|
|
1332
|
+
} catch {
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
1336
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
1337
|
+
let id = null;
|
|
1338
|
+
if (host === "youtu.be") id = videoId(parsed.pathname.split("/").filter(Boolean)[0]);
|
|
1339
|
+
if (host === "youtube.com" || host === "m.youtube.com" || host === "youtube-nocookie.com") {
|
|
1340
|
+
id = videoId(
|
|
1341
|
+
parsed.searchParams.get("v") ?? parsed.pathname.match(/^\/(?:embed|shorts)\/([^/?#]+)/)?.[1]
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
if (id) {
|
|
1345
|
+
return {
|
|
1346
|
+
kind: "iframe",
|
|
1347
|
+
src: `https://www.youtube-nocookie.com/embed/${id}`,
|
|
1348
|
+
provider: "YouTube"
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
if (host === "vimeo.com" || host === "player.vimeo.com") {
|
|
1352
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
1353
|
+
let vimeoId = null;
|
|
1354
|
+
for (let i = segments.length - 1; i >= 0; i -= 1) {
|
|
1355
|
+
if (/^\d+$/.test(segments[i])) {
|
|
1356
|
+
vimeoId = segments[i];
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (vimeoId) {
|
|
1361
|
+
return {
|
|
1362
|
+
kind: "iframe",
|
|
1363
|
+
src: `https://player.vimeo.com/video/${vimeoId}`,
|
|
1364
|
+
provider: "Vimeo"
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (/\.(?:m4v|mp4|og[gv]|webm)$/i.test(parsed.pathname)) {
|
|
1369
|
+
return { kind: "video", src: parsed.toString(), provider: "Video file" };
|
|
1370
|
+
}
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// ../richtext/src/embed.ts
|
|
1375
|
+
var EMBED_ASPECTS = ["16:9", "4:3", "1:1", "9:16"];
|
|
1376
|
+
var isEmbedAspect = (v) => EMBED_ASPECTS.includes(v);
|
|
1377
|
+
function detectEmbed(url) {
|
|
1378
|
+
const u = url.trim();
|
|
1379
|
+
let host = "";
|
|
1380
|
+
try {
|
|
1381
|
+
host = new URL(u).hostname.replace(/^www\./, "");
|
|
1382
|
+
} catch {
|
|
1383
|
+
host = "";
|
|
1384
|
+
}
|
|
1385
|
+
const is = (re) => re.test(u);
|
|
1386
|
+
if (is(/(?:youtube\.com\/(?:watch\?|embed\/|shorts\/|live\/)|youtu\.be\/)/i)) return { provider: "youtube", label: "YouTube", host };
|
|
1387
|
+
if (is(/vimeo\.com\/(?:video\/)?\d+/i)) return { provider: "vimeo", label: "Vimeo", host };
|
|
1388
|
+
if (is(/(?:twitter|x)\.com\/[^/]+\/status\/\d+/i)) return { provider: "x", label: "X post", host };
|
|
1389
|
+
if (is(/codepen\.io\/[^/]+\/(?:pen|embed)\//i)) return { provider: "codepen", label: "CodePen", host };
|
|
1390
|
+
if (is(/figma\.com\/(?:file|design|proto|board)\//i)) return { provider: "figma", label: "Figma", host };
|
|
1391
|
+
if (is(/(?:google\.[a-z.]+\/maps|maps\.app\.goo\.gl|goo\.gl\/maps)/i)) return { provider: "maps", label: "Google Maps", host };
|
|
1392
|
+
if (is(/open\.spotify\.com\/(?:episode|track|show|playlist|album)\//i)) return { provider: "spotify", label: "Spotify", host };
|
|
1393
|
+
if (is(/podcasts\.apple\.com\//i)) return { provider: "podcast", label: "Apple Podcasts", host };
|
|
1394
|
+
if (is(/\.(?:mp3|m4a|wav|ogg|aac)(?:\?|$)/i)) return { provider: "audio", label: "Audio", host };
|
|
1395
|
+
return { provider: "link", label: host || "Link", host };
|
|
1396
|
+
}
|
|
1397
|
+
function detectMore(url) {
|
|
1398
|
+
const host = url.hostname.toLowerCase().replace(/^www\./, "");
|
|
1399
|
+
const path = url.pathname;
|
|
1400
|
+
if (host === "youtube-nocookie.com" && path.startsWith("/embed/")) return { provider: "youtube", label: "YouTube" };
|
|
1401
|
+
if (host === "open.spotify.com" && /^\/embed\/(?:episode|track|show|playlist|album)\//.test(path)) return { provider: "spotify", label: "Spotify" };
|
|
1402
|
+
if (host === "calendly.com") return { provider: "calendly", label: "Calendly" };
|
|
1403
|
+
if (host.endsWith(".typeform.com") && /^\/to\//.test(path)) return { provider: "typeform", label: "Typeform" };
|
|
1404
|
+
if (host === "loom.com" && /^\/(?:share|embed)\//.test(path)) return { provider: "loom", label: "Loom" };
|
|
1405
|
+
if (host === "airtable.com") return { provider: "airtable", label: "Airtable" };
|
|
1406
|
+
if (host === "docs.google.com") {
|
|
1407
|
+
const kind = path.split("/")[1];
|
|
1408
|
+
const label = { forms: "Google Forms", document: "Google Docs", spreadsheets: "Google Sheets", presentation: "Google Slides" }[kind ?? ""];
|
|
1409
|
+
if (label) return { provider: "google", label };
|
|
1410
|
+
}
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
var FRAME_SIZE = {
|
|
1414
|
+
"16:9": ["640", "360"],
|
|
1415
|
+
"4:3": ["640", "480"],
|
|
1416
|
+
"1:1": ["640", "640"],
|
|
1417
|
+
"9:16": ["360", "640"]
|
|
1418
|
+
};
|
|
1419
|
+
var ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
1420
|
+
var FULL_WIDTH_HEIGHT = { calendly: "700", typeform: "500", airtable: "533" };
|
|
1421
|
+
function frameOf(provider, url) {
|
|
1422
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
1423
|
+
switch (provider) {
|
|
1424
|
+
case "youtube":
|
|
1425
|
+
case "vimeo": {
|
|
1426
|
+
const video = resolveVideoSource(url.href);
|
|
1427
|
+
return video?.kind === "iframe" ? { src: video.src } : null;
|
|
1428
|
+
}
|
|
1429
|
+
case "codepen": {
|
|
1430
|
+
const [user, mode, id] = parts;
|
|
1431
|
+
return user && ID.test(user) && (mode === "pen" || mode === "embed") && id && ID.test(id) ? { src: `https://codepen.io/${user}/embed/${id}?default-tab=result` } : null;
|
|
1432
|
+
}
|
|
1433
|
+
case "figma": {
|
|
1434
|
+
const [mode, key] = parts;
|
|
1435
|
+
if (!mode || !["file", "design", "proto", "board"].includes(mode) || !key || !ID.test(key)) return null;
|
|
1436
|
+
const frame = new URL(`https://embed.figma.com/${mode}/${key}`);
|
|
1437
|
+
const node = url.searchParams.get("node-id");
|
|
1438
|
+
if (node && /^[0-9A-Za-z:_-]{1,64}$/.test(node)) frame.searchParams.set("node-id", node);
|
|
1439
|
+
frame.searchParams.set("embed-host", "bettercms");
|
|
1440
|
+
return { src: frame.href };
|
|
1441
|
+
}
|
|
1442
|
+
case "maps": {
|
|
1443
|
+
if (!/^(?:www\.)?google\.[a-z.]+$/i.test(url.hostname)) return null;
|
|
1444
|
+
const pb = url.searchParams.get("pb");
|
|
1445
|
+
if (parts[0] === "maps" && parts[1] === "embed" && pb && /^[!A-Za-z0-9._:%+-]{1,2000}$/.test(pb)) {
|
|
1446
|
+
return { src: `https://www.google.com/maps/embed?pb=${encodeURIComponent(pb)}` };
|
|
1447
|
+
}
|
|
1448
|
+
const at = /@(-?\d{1,3}(?:\.\d+)?),(-?\d{1,3}(?:\.\d+)?)/.exec(url.pathname);
|
|
1449
|
+
const place = parts[0] === "maps" && parts[1] === "place" && parts[2] ? decodeURIComponent(parts[2]).replace(/\+/g, " ") : null;
|
|
1450
|
+
const q = at ? `${at[1]},${at[2]}` : url.searchParams.get("q") ?? place;
|
|
1451
|
+
if (!q || q.length > 200) return null;
|
|
1452
|
+
return { src: `https://maps.google.com/maps?q=${encodeURIComponent(q)}&output=embed` };
|
|
1453
|
+
}
|
|
1454
|
+
case "spotify": {
|
|
1455
|
+
const [type, id] = parts[0] === "embed" ? parts.slice(1) : parts;
|
|
1456
|
+
if (!type || !["episode", "track", "show", "playlist", "album"].includes(type) || !id || !ID.test(id)) return null;
|
|
1457
|
+
return { src: `https://open.spotify.com/embed/${type}/${id}`, height: type === "track" || type === "episode" ? "152" : "352" };
|
|
1458
|
+
}
|
|
1459
|
+
case "podcast":
|
|
1460
|
+
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;
|
|
1461
|
+
case "calendly":
|
|
1462
|
+
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;
|
|
1463
|
+
case "typeform": {
|
|
1464
|
+
const id = parts[0] === "to" ? parts[1] : void 0;
|
|
1465
|
+
return id && /^[A-Za-z0-9]{4,32}$/.test(id) ? { src: `https://form.typeform.com/to/${id}` } : null;
|
|
1466
|
+
}
|
|
1467
|
+
case "loom": {
|
|
1468
|
+
const id = parts[1];
|
|
1469
|
+
return (parts[0] === "share" || parts[0] === "embed") && id && /^[0-9a-f]{32}$/.test(id) ? { src: `https://www.loom.com/embed/${id}` } : null;
|
|
1470
|
+
}
|
|
1471
|
+
case "airtable": {
|
|
1472
|
+
const ids = parts[0] === "embed" ? parts.slice(1) : parts;
|
|
1473
|
+
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;
|
|
1474
|
+
}
|
|
1475
|
+
case "google": {
|
|
1476
|
+
const [kind, d, maybeE, maybeId] = parts;
|
|
1477
|
+
if (d !== "d") return null;
|
|
1478
|
+
const published = maybeE === "e";
|
|
1479
|
+
const id = published ? maybeId : maybeE;
|
|
1480
|
+
if (!id || !/^[A-Za-z0-9_-]{10,128}$/.test(id)) return null;
|
|
1481
|
+
const base = `https://docs.google.com/${kind}/d/${published ? `e/${id}` : id}`;
|
|
1482
|
+
if (kind === "forms") return { src: `${base}/viewform?embedded=true`, height: "800" };
|
|
1483
|
+
if (kind === "document") return { src: published ? `${base}/pub?embedded=true` : `${base}/preview`, height: "600" };
|
|
1484
|
+
if (kind === "spreadsheets") return { src: published ? `${base}/pubhtml?widget=true&headers=false` : `${base}/preview`, height: "600" };
|
|
1485
|
+
if (kind === "presentation") return { src: published ? `${base}/embed` : `${base}/preview` };
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
default:
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
var X_STATUS = /^\/([A-Za-z0-9_]{1,15})\/status\/(\d{1,25})(?:\/|$)/;
|
|
1493
|
+
var X_HOSTS = /^(?:www\.|mobile\.)?(?:twitter|x)\.com$/i;
|
|
1494
|
+
function canonicalEmbedUrl(raw) {
|
|
1495
|
+
const url = typeof raw === "string" ? raw.trim() : "";
|
|
1496
|
+
const safe = url ? safeHref(url) : null;
|
|
1497
|
+
if (!safe || !/^https?:\/\//i.test(safe)) return null;
|
|
1498
|
+
let parsed;
|
|
1499
|
+
try {
|
|
1500
|
+
parsed = new URL(safe);
|
|
1501
|
+
} catch {
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
const status = X_HOSTS.test(parsed.hostname) ? X_STATUS.exec(parsed.pathname) : null;
|
|
1505
|
+
if (status) return `https://x.com/${status[1].toLowerCase()}/status/${status[2]}`;
|
|
1506
|
+
parsed.hash = "";
|
|
1507
|
+
return parsed.href;
|
|
1508
|
+
}
|
|
1509
|
+
function resolveEmbed(rawUrl, aspect = "16:9") {
|
|
1510
|
+
const url = typeof rawUrl === "string" ? rawUrl.trim() : "";
|
|
1511
|
+
const v2 = detectEmbed(url);
|
|
1512
|
+
const safe = url ? safeHref(url) : null;
|
|
1513
|
+
let parsed = null;
|
|
1514
|
+
try {
|
|
1515
|
+
parsed = safe && /^https?:\/\//i.test(safe) ? new URL(safe) : null;
|
|
1516
|
+
} catch {
|
|
1517
|
+
parsed = null;
|
|
1518
|
+
}
|
|
1519
|
+
const { provider, label } = v2.provider === "link" && parsed ? detectMore(parsed) ?? v2 : v2;
|
|
1520
|
+
const link = { kind: "link", provider, label, url: safe };
|
|
1521
|
+
if (!parsed) return link;
|
|
1522
|
+
if (provider === "x") return { kind: "card", provider: "x", label, url: canonicalEmbedUrl(parsed.href) ?? parsed.href };
|
|
1523
|
+
if (provider === "audio") return parsed.protocol === "https:" ? { kind: "audio", provider: "audio", label, src: parsed.href } : link;
|
|
1524
|
+
const frame = frameOf(provider, parsed);
|
|
1525
|
+
if (!frame) return link;
|
|
1526
|
+
const height = frame.height ?? FULL_WIDTH_HEIGHT[provider];
|
|
1527
|
+
if (height) return { kind: "frame", provider, label, src: frame.src, width: "100%", height };
|
|
1528
|
+
const [width, h] = FRAME_SIZE[isEmbedAspect(aspect) ? aspect : "16:9"];
|
|
1529
|
+
return { kind: "frame", provider, label, src: frame.src, width, height: h };
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1236
1532
|
// src/blocks/portableText.tsx
|
|
1237
1533
|
import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1238
1534
|
var TOKEN = /^[a-z0-9-]+$/i;
|
|
@@ -1364,8 +1660,69 @@ function Table({ value }) {
|
|
|
1364
1660
|
] }) : notes ? /* @__PURE__ */ jsx14("p", { "data-notes": "", id: notesId, children: notes }) : null
|
|
1365
1661
|
] }) : table;
|
|
1366
1662
|
}
|
|
1367
|
-
function
|
|
1663
|
+
function CheckItem({ checked, address, children }) {
|
|
1664
|
+
return /* @__PURE__ */ jsxs11("li", { ...address, "data-checked": String(checked), children: [
|
|
1665
|
+
/* @__PURE__ */ jsx14("input", { type: "checkbox", disabled: true, defaultChecked: checked }),
|
|
1666
|
+
children
|
|
1667
|
+
] });
|
|
1668
|
+
}
|
|
1669
|
+
function XCard({ url, card }) {
|
|
1670
|
+
return /* @__PURE__ */ jsxs11("blockquote", { className: "twitter-tweet bcms-embed-card", cite: url, children: [
|
|
1671
|
+
/* @__PURE__ */ jsx14("p", { children: card.text }),
|
|
1672
|
+
"\u2014 ",
|
|
1673
|
+
card.authorName,
|
|
1674
|
+
" ",
|
|
1675
|
+
/* @__PURE__ */ jsx14("a", { href: url, children: card.date || "View on X" })
|
|
1676
|
+
] });
|
|
1677
|
+
}
|
|
1678
|
+
function Toc({ value, outline }) {
|
|
1679
|
+
const { title, label, depth } = tocView(value);
|
|
1680
|
+
const link = (o) => /* @__PURE__ */ jsx14("a", { href: `#${o.id}`, children: o.text });
|
|
1681
|
+
return /* @__PURE__ */ jsxs11("nav", { "data-rt": "toc", "data-depth": depth, "aria-label": label, children: [
|
|
1682
|
+
title ? /* @__PURE__ */ jsx14("p", { "data-title": "", children: title }) : null,
|
|
1683
|
+
/* @__PURE__ */ jsx14("ol", { children: tocTree(outline.items, depth).map((e) => /* @__PURE__ */ jsxs11("li", { children: [
|
|
1684
|
+
link(e),
|
|
1685
|
+
e.children.length ? /* @__PURE__ */ jsx14("ol", { children: e.children.map((c) => /* @__PURE__ */ jsx14("li", { children: link(c) }, c.id)) }) : null
|
|
1686
|
+
] }, e.id)) })
|
|
1687
|
+
] });
|
|
1688
|
+
}
|
|
1689
|
+
function Gallery({ value }) {
|
|
1690
|
+
const { layout, images } = galleryView(value);
|
|
1691
|
+
const s = GALLERY_STYLES[layout];
|
|
1692
|
+
return /* @__PURE__ */ jsx14("figure", { "data-rt": "gallery", "data-layout": layout, children: /* @__PURE__ */ jsx14("ul", { style: styleObject(s.ul), tabIndex: layout === "carousel" ? 0 : void 0, children: images.map((x) => /* @__PURE__ */ jsx14("li", { style: styleObject(s.li), children: /* @__PURE__ */ jsxs11("figure", { style: styleObject(s.figure), children: [
|
|
1693
|
+
/* @__PURE__ */ jsx14("img", { src: x.src, alt: x.alt, style: styleObject(s.img), loading: "lazy" }),
|
|
1694
|
+
x.caption ? /* @__PURE__ */ jsx14("figcaption", { children: x.caption }) : null
|
|
1695
|
+
] }) }, x.key)) }) });
|
|
1696
|
+
}
|
|
1697
|
+
function Embed({ value, embeds }) {
|
|
1698
|
+
const v = value ?? {};
|
|
1699
|
+
const url = typeof v.url === "string" ? v.url.trim() : "";
|
|
1700
|
+
const aspect = isEmbedAspect(v.aspect) ? v.aspect : "16:9";
|
|
1701
|
+
const title = typeof v.title === "string" ? v.title.trim() : "";
|
|
1702
|
+
const r = resolveEmbed(url, aspect);
|
|
1703
|
+
const label = title || r.label;
|
|
1704
|
+
const card = r.kind === "card" ? embeds.cards?.[r.url] : void 0;
|
|
1705
|
+
let body = null;
|
|
1706
|
+
if (r.kind === "frame") {
|
|
1707
|
+
body = /* @__PURE__ */ jsx14("iframe", { src: r.src, title: label, width: r.width, height: r.height, loading: "lazy", referrerPolicy: "strict-origin-when-cross-origin", allowFullScreen: true });
|
|
1708
|
+
} else if (r.kind === "audio") {
|
|
1709
|
+
body = /* @__PURE__ */ jsx14("audio", { controls: true, preload: "none", src: r.src, title: label });
|
|
1710
|
+
} else if (r.kind === "card" && card) {
|
|
1711
|
+
body = /* @__PURE__ */ jsx14(XCard, { url: r.url, card });
|
|
1712
|
+
} else if (r.url) {
|
|
1713
|
+
body = /* @__PURE__ */ jsx14("a", { href: r.url, children: label });
|
|
1714
|
+
}
|
|
1715
|
+
return /* @__PURE__ */ jsx14("figure", { "data-rt": "embed", "data-url": url, "data-provider": r.provider, "data-aspect": aspect, "aria-label": title, children: body });
|
|
1716
|
+
}
|
|
1717
|
+
function buildComponents(renderBlock, embeds = {}, outline = { items: [], nodes: /* @__PURE__ */ new WeakMap() }) {
|
|
1368
1718
|
return {
|
|
1719
|
+
// RT5: every top-level h2 and h3 carries its outline id, as html.ts writes it.
|
|
1720
|
+
block: {
|
|
1721
|
+
h2: ({ value, children }) => /* @__PURE__ */ jsx14("h2", { id: outline.nodes.get(value), children }),
|
|
1722
|
+
h3: ({ value, children }) => /* @__PURE__ */ jsx14("h3", { id: outline.nodes.get(value), children })
|
|
1723
|
+
},
|
|
1724
|
+
list: { check: ({ children }) => /* @__PURE__ */ jsx14("ul", { "data-task": "true", children }) },
|
|
1725
|
+
listItem: { check: ({ value, children }) => /* @__PURE__ */ jsx14(CheckItem, { checked: value.checked === true, children }) },
|
|
1369
1726
|
types: {
|
|
1370
1727
|
[PT_TYPES.divider]: () => /* @__PURE__ */ jsx14("hr", {}),
|
|
1371
1728
|
[PT_TYPES.table]: ({ value }) => /* @__PURE__ */ jsx14(Table, { value }),
|
|
@@ -1415,6 +1772,9 @@ function buildComponents(renderBlock) {
|
|
|
1415
1772
|
] }) : null
|
|
1416
1773
|
] });
|
|
1417
1774
|
},
|
|
1775
|
+
[PT_TYPES.embed]: ({ value }) => /* @__PURE__ */ jsx14(Embed, { value, embeds }),
|
|
1776
|
+
[PT_TYPES.gallery]: ({ value }) => /* @__PURE__ */ jsx14(Gallery, { value }),
|
|
1777
|
+
[PT_TYPES.tableOfContents]: ({ value }) => /* @__PURE__ */ jsx14(Toc, { value, outline }),
|
|
1418
1778
|
/**
|
|
1419
1779
|
* Key takeaways — the React half of `portableTextToHtml`'s, and the same DOM: the V2 kit's
|
|
1420
1780
|
* `<aside data-rt="takeaways">`, read through the one `keyTakeawaysView` both renderers share.
|
|
@@ -1442,7 +1802,7 @@ function buildComponents(renderBlock) {
|
|
|
1442
1802
|
const href = typeof v.href === "string" ? safeHref(v.href) : null;
|
|
1443
1803
|
if (!href) return /* @__PURE__ */ jsx14(Fragment2, { children });
|
|
1444
1804
|
const blank = v.target === "_blank";
|
|
1445
|
-
return /* @__PURE__ */ jsx14("a", { href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
|
|
1805
|
+
return /* @__PURE__ */ jsx14("a", { ...v.button === true ? { "data-button": "true" } : {}, href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
|
|
1446
1806
|
},
|
|
1447
1807
|
/**
|
|
1448
1808
|
* The site's own inline markup, rendered as itself — the React half of
|
|
@@ -1531,15 +1891,22 @@ function calloutOrToggle(value) {
|
|
|
1531
1891
|
}
|
|
1532
1892
|
return null;
|
|
1533
1893
|
}
|
|
1534
|
-
function PortableText({ value, bindingBase, renderBlock }) {
|
|
1894
|
+
function PortableText({ value, bindingBase, renderBlock, embeds }) {
|
|
1535
1895
|
const blocks = React6.useMemo(() => normalizePortableText(readValue(value)), [value]);
|
|
1536
|
-
const
|
|
1896
|
+
const outline = React6.useMemo(() => outlineOf(blocks), [blocks]);
|
|
1897
|
+
const components = React6.useMemo(() => buildComponents(renderBlock, embeds, outline), [renderBlock, embeds, outline]);
|
|
1537
1898
|
const listComponents = React6.useMemo(() => {
|
|
1538
1899
|
if (!bindingBase) return null;
|
|
1539
1900
|
const keys = new Set(blocks.map((b) => b._key));
|
|
1540
1901
|
return {
|
|
1541
1902
|
...components,
|
|
1542
|
-
listItem: ({ value: item, children }) =>
|
|
1903
|
+
listItem: ({ value: item, children }) => {
|
|
1904
|
+
const address = keys.has(item._key) ? { "data-bcms-field": `${bindingBase}#${item._key}`, "data-bcms-kind": "richtext" } : void 0;
|
|
1905
|
+
if (item.listItem === "check") {
|
|
1906
|
+
return /* @__PURE__ */ jsx14(CheckItem, { checked: item.checked === true, address, children });
|
|
1907
|
+
}
|
|
1908
|
+
return /* @__PURE__ */ jsx14("li", { ...address, children });
|
|
1909
|
+
}
|
|
1543
1910
|
};
|
|
1544
1911
|
}, [bindingBase, blocks, components]);
|
|
1545
1912
|
if (blocks.length === 0) return null;
|