@bettercms-ai/ui 0.16.0 → 0.17.1

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.
@@ -19,8 +19,8 @@ interface BlocksRendererProps {
19
19
  annotate?: boolean;
20
20
  /**
21
21
  * The instant block windows (`block.window`) are read against. Defaults to the render time.
22
- * A block outside its window is not rendered, at any depth; with `annotate` it is rendered and
23
- * marked `data-bcms-window="pending|expired"` instead.
22
+ * A block outside its window, or hidden on every breakpoint, is not rendered, at any depth; with
23
+ * `annotate` it is rendered and marked `data-bcms-window="pending|expired"` / `data-bcms-hidden="all"`.
24
24
  */
25
25
  now?: Date;
26
26
  }
package/dist/renderer.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/renderer.tsx
4
- import { RESPONSIVE_CSS, hideOnAttr, splitButtonStyle, usesHideOn, visibleBlocks, windowState } from "@bettercms-ai/types";
4
+ import { RESPONSIVE_CSS, hiddenEverywhere, hideOnAttr, splitButtonStyle, usesHideOn, visibleBlocks, windowState } from "@bettercms-ai/types";
5
5
 
6
6
  // src/blocks/Heading.tsx
7
7
  import { jsx } from "react/jsx-runtime";
@@ -36,6 +36,13 @@ import React from "react";
36
36
  import { PortableText as PT } from "@portabletext/react";
37
37
 
38
38
  // ../richtext/src/types.ts
39
+ function pullQuoteHref(link) {
40
+ if (typeof link === "string") return link.trim();
41
+ if (link && typeof link === "object" && typeof link.href === "string") {
42
+ return link.href.trim();
43
+ }
44
+ return "";
45
+ }
39
46
  var PT_TYPES = {
40
47
  /** A generic embedded block: `{schemaKey, schemaLabel, fields}`, the successor to Lexical's BlockNode. */
41
48
  block: "bcmsBlock",
@@ -53,6 +60,10 @@ var PT_TYPES = {
53
60
  video: "video",
54
61
  /** A downloadable document — a PDF, a spec sheet. `<a href download>`, nothing more. */
55
62
  file: "file",
63
+ /** A pull quote — see `PortableTextPullQuote`. The V2 kit's `<figure data-rt="pullquote">`. */
64
+ pullQuote: "pullQuote",
65
+ /** Key takeaways — see `PortableTextKeyTakeaways`. The V2 kit's `<aside data-rt="takeaways">`. */
66
+ keyTakeaways: "keyTakeaways",
56
67
  /**
57
68
  * A table. Spelled `table`, unprefixed, because that is what every other Portable Text
58
69
  * producer calls one — the Sanity-shaped `{_type:"table", rows}` this platform used to refuse
@@ -77,8 +88,11 @@ var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
77
88
  PT_TYPES.divider,
78
89
  PT_TYPES.mention,
79
90
  PT_TYPES.video,
80
- PT_TYPES.file
91
+ PT_TYPES.file,
92
+ PT_TYPES.pullQuote,
93
+ PT_TYPES.keyTakeaways
81
94
  ]);
95
+ var EMBEDDED_COMPONENT_KEY = "component";
82
96
 
83
97
  // ../richtext/src/styled.ts
84
98
  var STYLED_TAGS = [
@@ -249,7 +263,8 @@ function normalizeCell(raw, at, mint, read) {
249
263
  content: Array.isArray(raw.content) ? read(raw.content, inner) : textContent(typeof raw.text === "string" ? raw.text : "", inner),
250
264
  ...raw.header === true ? { header: true } : {},
251
265
  ...span === void 0 ? {} : { span },
252
- ...rowSpan === void 0 ? {} : { rowSpan }
266
+ ...rowSpan === void 0 ? {} : { rowSpan },
267
+ ...typeof raw.group === "string" ? { group: raw.group } : {}
253
268
  };
254
269
  }
255
270
  function normalizeRow(raw, at, mint, read) {
@@ -257,7 +272,8 @@ function normalizeRow(raw, at, mint, read) {
257
272
  return {
258
273
  _key: isRecord(raw) && isValidKey(raw._key) ? raw._key : mint("r", at),
259
274
  cells: cells.map((c, col) => normalizeCell(c, [at, col], mint, read)),
260
- ...isRecord(raw) && raw.head === true ? { head: true } : {}
275
+ ...isRecord(raw) && raw.head === true ? { head: true } : {},
276
+ ...isRecord(raw) && typeof raw.group === "string" ? { group: raw.group } : {}
261
277
  };
262
278
  }
263
279
  function builtInTableToTable(node, key = randomKey) {
@@ -302,24 +318,93 @@ function migrateTable(node, key = randomKey, read = asIs) {
302
318
  rows: rows.map((r, i) => normalizeRow(r, i, mint, read)),
303
319
  // Carried through: a migrated table that an author saves back is still the same table, and
304
320
  // dropping the flag on that save would change the page the next time it renders.
305
- ...node.legacy === true ? { legacy: true } : {}
321
+ ...node.legacy === true ? { legacy: true } : {},
322
+ ...tableExtras(node)
323
+ };
324
+ }
325
+ function tableExtras(node) {
326
+ const groups = (list) => Array.isArray(list) ? list.filter((g) => isRecord(g) && typeof g._key === "string" && g._key !== "").map((g) => ({ _key: g._key, label: typeof g.label === "string" ? g.label : "" })) : void 0;
327
+ const rowGroups = groups(node.rowGroups);
328
+ const columnGroups = groups(node.columnGroups);
329
+ return {
330
+ ...typeof node.caption === "string" ? { caption: node.caption } : {},
331
+ ...typeof node.notes === "string" ? { notes: node.notes } : {},
332
+ ...rowGroups ? { rowGroups } : {},
333
+ ...columnGroups ? { columnGroups } : {}
334
+ };
335
+ }
336
+ function tableGroupLayout(table) {
337
+ const rows = Array.isArray(table.rows) ? table.rows : [];
338
+ const labels = (list) => new Map(
339
+ (Array.isArray(list) ? list : []).filter((g) => isRecord(g) && typeof g._key === "string").map((g) => [g._key, typeof g.label === "string" ? g.label : ""])
340
+ );
341
+ const cellsOf = (row) => isRecord(row) && Array.isArray(row.cells) ? row.cells : [];
342
+ const spanOf2 = (cell) => isRecord(cell) && typeof cell.span === "number" && Number.isInteger(cell.span) && cell.span > 1 ? cell.span : 1;
343
+ const width = rows.reduce((max, row) => Math.max(max, cellsOf(row).reduce((n, c) => n + spanOf2(c), 0)), 0);
344
+ const colLabels = labels(table.columnGroups);
345
+ const columns = [];
346
+ for (const cell of cellsOf(rows[0])) {
347
+ const key = isRecord(cell) && typeof cell.group === "string" && colLabels.has(cell.group) ? cell.group : void 0;
348
+ const last = columns[columns.length - 1];
349
+ if (last && last.key === key) last.width += spanOf2(cell);
350
+ else columns.push({ width: spanOf2(cell), ...key === void 0 ? {} : { key, label: colLabels.get(key) } });
351
+ }
352
+ const rowLabels = labels(table.rowGroups);
353
+ const headCount = rows.findIndex((r) => !isRecord(r) || r.head !== true);
354
+ const body = headCount === -1 ? [] : rows.slice(headCount);
355
+ const runs = [];
356
+ for (const row of body) {
357
+ const key = isRecord(row) && typeof row.group === "string" && rowLabels.has(row.group) ? row.group : void 0;
358
+ const last = runs[runs.length - 1];
359
+ if (last && last.key === key) last.rows.push(row);
360
+ else runs.push({ rows: [row], ...key === void 0 ? {} : { key, label: rowLabels.get(key) } });
361
+ }
362
+ return {
363
+ columns: columns.some((r) => r.key !== void 0) ? columns : [],
364
+ rows: runs.some((r) => r.key !== void 0) ? runs : [],
365
+ width
306
366
  };
307
367
  }
308
368
 
309
- // ../richtext/src/read.ts
369
+ // ../richtext/src/key-takeaways.ts
310
370
  var isRecord2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
371
+ function keyTakeawaysView(value) {
372
+ const v = isRecord2(value) ? value : {};
373
+ const title = typeof v.title === "string" ? v.title.trim() : "";
374
+ const points = (Array.isArray(v.points) ? v.points : []).map((p) => typeof p === "string" ? p.trim() : "").filter(Boolean);
375
+ return { title, points };
376
+ }
377
+
378
+ // ../richtext/src/component-copy.ts
379
+ var isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
380
+ var fieldsOf = (value) => isRecord3(value) && value.schemaKey === EMBEDDED_COMPONENT_KEY && isRecord3(value.fields) ? value.fields : null;
381
+ function detachedComponentCopy(value) {
382
+ const f = fieldsOf(value);
383
+ if (!f || f.detached !== true || !Array.isArray(f.blocks)) return null;
384
+ return {
385
+ id: isRecord3(value) && typeof value._key === "string" ? value._key : "",
386
+ componentId: typeof f.componentId === "string" ? f.componentId : "",
387
+ props: isRecord3(f.props) ? f.props : {},
388
+ blocks: f.blocks,
389
+ ...typeof f.sourcePublishedAt === "string" ? { sourcePublishedAt: f.sourcePublishedAt } : {},
390
+ ...typeof f.variant === "string" ? { variant: f.variant } : {}
391
+ };
392
+ }
393
+
394
+ // ../richtext/src/read.ts
395
+ var isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
311
396
  function normalizePortableText(value, key = randomKey) {
312
397
  if (!Array.isArray(value)) return [];
313
398
  return value.flatMap((raw) => {
314
- if (!isRecord2(raw) || typeof raw._type !== "string") return [];
399
+ if (!isRecord4(raw) || typeof raw._type !== "string") return [];
315
400
  const table = migrateTable(raw, key, (v, cellKey) => normalizePortableText(v, cellKey));
316
401
  if (table) return [table];
317
- const node = { ...raw, _key: isRecord2(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
402
+ const node = { ...raw, _key: isRecord4(raw) && typeof raw._key === "string" && raw._key ? raw._key : key() };
318
403
  if (node._type === "block") {
319
404
  if (!Array.isArray(node.markDefs)) node.markDefs = [];
320
405
  if (typeof node.style !== "string") node.style = "normal";
321
406
  node.children = (Array.isArray(node.children) ? node.children : []).map((c) => {
322
- if (!isRecord2(c)) return c;
407
+ if (!isRecord4(c)) return c;
323
408
  const child = { ...c, _key: typeof c._key === "string" && c._key ? c._key : key() };
324
409
  if (child._type === "span" && !Array.isArray(child.marks)) child.marks = [];
325
410
  return child;
@@ -329,9 +414,9 @@ function normalizePortableText(value, key = randomKey) {
329
414
  });
330
415
  }
331
416
  function readStructuredContent(props) {
332
- if (!isRecord2(props)) return null;
417
+ if (!isRecord4(props)) return null;
333
418
  const candidate = props.content ?? props.value;
334
- const arr = Array.isArray(candidate) ? candidate : isRecord2(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
419
+ const arr = Array.isArray(candidate) ? candidate : isRecord4(candidate) ? Array.isArray(candidate.content) ? candidate.content : Array.isArray(candidate.value) ? candidate.value : null : null;
335
420
  return arr && arr.length > 0 ? arr : null;
336
421
  }
337
422
 
@@ -368,19 +453,40 @@ function Table({ value }) {
368
453
  if (rows.length === 0) return null;
369
454
  const cells = (row) => (Array.isArray(row?.cells) ? row.cells : []).map((cell) => {
370
455
  const Tag = cell?.header === true ? "th" : "td";
371
- const span = typeof cell?.span === "number" && cell.span > 1 ? cell.span : void 0;
456
+ const span2 = typeof cell?.span === "number" && cell.span > 1 ? cell.span : void 0;
372
457
  const rowSpan = typeof cell?.rowSpan === "number" && cell.rowSpan > 1 ? cell.rowSpan : void 0;
373
458
  const content = Array.isArray(cell?.content) ? cell.content : [];
374
459
  const lone = content.length === 1 && content[0]._type === "block" && content[0].style === "normal" && !content[0].listItem;
375
- return /* @__PURE__ */ jsx2(Tag, { ...span ? { colSpan: span } : {}, ...rowSpan ? { rowSpan } : {}, children: /* @__PURE__ */ jsx2(PT, { value: content, components: lone ? inline : components }) }, cell?._key);
460
+ return /* @__PURE__ */ jsx2(Tag, { ...span2 ? { colSpan: span2 } : {}, ...rowSpan ? { rowSpan } : {}, children: /* @__PURE__ */ jsx2(PT, { value: content, components: lone ? inline : components }) }, cell?._key);
376
461
  });
377
- const rendered = rows.map((row) => /* @__PURE__ */ jsx2("tr", { children: cells(row) }, row?._key));
462
+ const rowEl = (row) => /* @__PURE__ */ jsx2("tr", { children: cells(row) }, row?._key);
378
463
  const headCount = rows.findIndex((r) => r?.head !== true);
379
464
  const head = headCount === -1 ? rows.length : headCount;
380
- return /* @__PURE__ */ jsxs("table", { ...value.legacy === true ? { className: "bcms-table" } : {}, children: [
381
- head > 0 && /* @__PURE__ */ jsx2("thead", { children: rendered.slice(0, head) }),
382
- head < rows.length && /* @__PURE__ */ jsx2("tbody", { children: rendered.slice(head) })
465
+ const layout = tableGroupLayout(value);
466
+ const caption = typeof value.caption === "string" ? value.caption.trim() : "";
467
+ const notes = typeof value.notes === "string" ? value.notes.trim() : "";
468
+ const notesId = notes && typeof value._key === "string" && value._key ? `${value._key}-notes` : void 0;
469
+ const span = (n) => n > 1 ? n : void 0;
470
+ const groupRow = layout.columns.length ? /* @__PURE__ */ jsx2("tr", { children: layout.columns.flatMap(
471
+ (run, i) => run.label === void 0 ? Array.from({ length: run.width }, (_, k) => /* @__PURE__ */ jsx2("td", {}, `${i}-${k}`)) : [/* @__PURE__ */ jsx2("th", { scope: "colgroup", colSpan: span(run.width), "data-group": run.key, children: run.label }, i)]
472
+ ) }) : null;
473
+ const bodies = layout.rows.length ? layout.rows.map((run, i) => /* @__PURE__ */ jsxs("tbody", { "data-key": run.key, children: [
474
+ run.label !== void 0 && /* @__PURE__ */ jsx2("tr", { children: /* @__PURE__ */ jsx2("th", { scope: "rowgroup", colSpan: span(layout.width), children: run.label }) }),
475
+ run.rows.map(rowEl)
476
+ ] }, i)) : head < rows.length && /* @__PURE__ */ jsx2("tbody", { children: rows.slice(head).map(rowEl) });
477
+ const table = /* @__PURE__ */ jsxs("table", { ...value.legacy === true ? { className: "bcms-table" } : {}, ...notesId ? { "aria-describedby": notesId } : {}, children: [
478
+ caption && /* @__PURE__ */ jsx2("caption", { children: caption }),
479
+ layout.columns.map((run, i) => /* @__PURE__ */ jsx2("colgroup", { span: span(run.width) }, i)),
480
+ (groupRow || head > 0) && /* @__PURE__ */ jsxs("thead", { children: [
481
+ groupRow,
482
+ rows.slice(0, head).map(rowEl)
483
+ ] }),
484
+ bodies
383
485
  ] });
486
+ return notes ? /* @__PURE__ */ jsxs("figure", { "data-rt": "table", children: [
487
+ table,
488
+ /* @__PURE__ */ jsx2("p", { "data-notes": "", id: notesId, children: notes })
489
+ ] }) : table;
384
490
  }
385
491
  function buildComponents(renderBlock) {
386
492
  return {
@@ -402,10 +508,47 @@ function buildComponents(renderBlock) {
402
508
  [PT_TYPES.block]: ({ value }) => {
403
509
  const rendered = renderBlock?.(value);
404
510
  if (rendered !== void 0) return /* @__PURE__ */ jsx2(Fragment, { children: rendered });
511
+ const copy = detachedComponentCopy(value);
512
+ if (copy) {
513
+ return /* @__PURE__ */ jsx2("div", { className: "bcms-block bcms-block--component", "data-bcms-component-copy": copy.componentId, "data-bcms-instance": copy.id, children: /* @__PURE__ */ jsx2(BlocksRenderer, { blocks: copy.blocks }) });
514
+ }
405
515
  const v = value;
406
516
  const key = typeof v.schemaKey === "string" ? v.schemaKey : "block";
407
517
  return /* @__PURE__ */ jsx2("div", { className: "bcms-block", "data-bcms-schema-key": key });
408
518
  },
519
+ /**
520
+ * A pull quote — the React half of `portableTextToHtml`'s, and the same DOM: the V2 kit's
521
+ * `<figure data-rt="pullquote">`, the name linked (and the quote cited) only through
522
+ * `safeHref`. pull-quote.test.tsx compares the two.
523
+ */
524
+ [PT_TYPES.pullQuote]: ({ value }) => {
525
+ const v = value;
526
+ const text = (x) => typeof x === "string" ? x.trim() : "";
527
+ const quote = typeof v.quote === "string" ? v.quote : "";
528
+ const name = text(v.name);
529
+ const role = text(v.role);
530
+ const href = safeHref(pullQuoteHref(v.link)) ?? void 0;
531
+ return /* @__PURE__ */ jsxs("figure", { "data-rt": "pullquote", children: [
532
+ /* @__PURE__ */ jsx2("blockquote", { cite: href, children: /* @__PURE__ */ jsx2("p", { children: quote }) }),
533
+ name || role ? /* @__PURE__ */ jsxs("figcaption", { children: [
534
+ name ? href ? /* @__PURE__ */ jsx2("a", { href, "data-name": "", children: name }) : /* @__PURE__ */ jsx2("span", { "data-name": "", children: name }) : null,
535
+ name && role ? ", " : null,
536
+ role ? /* @__PURE__ */ jsx2("span", { "data-role": "", children: role }) : null
537
+ ] }) : null
538
+ ] });
539
+ },
540
+ /**
541
+ * Key takeaways — the React half of `portableTextToHtml`'s, and the same DOM: the V2 kit's
542
+ * `<aside data-rt="takeaways">`, read through the one `keyTakeawaysView` both renderers share.
543
+ * portable-text-key-takeaways.test.tsx compares the two.
544
+ */
545
+ [PT_TYPES.keyTakeaways]: ({ value }) => {
546
+ const { title, points } = keyTakeawaysView(value);
547
+ return /* @__PURE__ */ jsxs("aside", { "data-rt": "takeaways", children: [
548
+ title ? /* @__PURE__ */ jsx2("p", { "data-title": "", children: title }) : null,
549
+ /* @__PURE__ */ jsx2("ul", { children: points.map((p, i) => /* @__PURE__ */ jsx2("li", { children: p }, i)) })
550
+ ] });
551
+ },
409
552
  [PT_TYPES.mention]: ({ value }) => {
410
553
  const v = value;
411
554
  return /* @__PURE__ */ jsx2("span", { className: "bcms-mention", children: typeof v.label === "string" ? v.label : "" });
@@ -989,8 +1132,9 @@ function styleOf(block) {
989
1132
  function withHide(block, wrapperStyle, el, stampAt) {
990
1133
  const hide = hideOnAttr(block.style);
991
1134
  const state = stampAt ? windowState(block, stampAt) : "live";
992
- if (!wrapperStyle && !hide && state === "live") return el;
993
- return /* @__PURE__ */ jsx15("div", { style: wrapperStyle, ...hide ? { "data-bcms-hide": hide } : {}, ...state === "live" ? {} : { "data-bcms-window": state }, children: el });
1135
+ const hidden = !!stampAt && hiddenEverywhere(block);
1136
+ if (!wrapperStyle && !hide && state === "live" && !hidden) return el;
1137
+ return /* @__PURE__ */ jsx15("div", { style: wrapperStyle, ...hide ? { "data-bcms-hide": hide } : {}, ...state === "live" ? {} : { "data-bcms-window": state }, ...hidden ? { "data-bcms-hidden": "all" } : {}, children: el });
994
1138
  }
995
1139
  function bindingProps(block, annotate) {
996
1140
  if (!annotate || !block.id) return {};