@bendyline/squisq 2.13.14 → 2.13.16

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.
Files changed (43) hide show
  1. package/dist/{Doc-BhvotZx_.d.ts → Doc-DLfTam0R.d.ts} +82 -6
  2. package/dist/{ImageEditDoc-D8hYio6b.d.ts → ImageEditDoc-DdSHb6sj.d.ts} +1 -1
  3. package/dist/{chunk-YQQWE4HG.js → chunk-33CAD575.js} +200 -262
  4. package/dist/{chunk-L7QZORLB.js → chunk-37RLFDIT.js} +11 -4
  5. package/dist/chunk-4R3YZTPX.js +42 -0
  6. package/dist/{chunk-DBVCSVNV.js → chunk-A4ZFYGU6.js} +13 -38
  7. package/dist/{chunk-CTT4XAGO.js → chunk-BG2OYBGC.js} +2534 -737
  8. package/dist/{chunk-2SRF5III.js → chunk-BVL3F64S.js} +1 -1
  9. package/dist/chunk-C7DKMO4H.js +88 -0
  10. package/dist/{chunk-SGNWO7OF.js → chunk-MA7XUCSQ.js} +517 -30
  11. package/dist/{chunk-JSGIWW35.js → chunk-MTIR5PWF.js} +1 -1
  12. package/dist/{chunk-C4SB3NN4.js → chunk-NBDF6SQ5.js} +22 -2
  13. package/dist/chunk-TOPEP5B7.js +515 -0
  14. package/dist/{chunk-OIQ2GO2O.js → chunk-TXN23SG2.js} +1 -1
  15. package/dist/{chunk-7MS26OXD.js → chunk-UDPPWYYM.js} +6 -0
  16. package/dist/{chunk-J2G6EOEJ.js → chunk-XY5Z23C4.js} +75 -16
  17. package/dist/{chunk-5DFFVW75.js → chunk-Z73DG6IL.js} +1 -1
  18. package/dist/doc/index.d.ts +412 -26
  19. package/dist/doc/index.js +66 -11
  20. package/dist/fence/index.d.ts +1 -1
  21. package/dist/generate/index.d.ts +1 -1
  22. package/dist/imageEdit/index.d.ts +3 -3
  23. package/dist/index.d.ts +9 -9
  24. package/dist/index.js +147 -29
  25. package/dist/jsonForm/index.d.ts +1 -1
  26. package/dist/jsonForm/index.js +5 -4
  27. package/dist/markdown/index.js +3 -3
  28. package/dist/{materializePageSection-CWxou4mX.d.ts → materializePageSection-HIoRYiZu.d.ts} +121 -8
  29. package/dist/mediaEdit/index.d.ts +154 -3
  30. package/dist/mediaEdit/index.js +241 -2
  31. package/dist/narration/index.d.ts +1 -1
  32. package/dist/narration/index.js +7 -5
  33. package/dist/recommend/index.d.ts +129 -2
  34. package/dist/recommend/index.js +13 -4
  35. package/dist/schemas/index.d.ts +26 -7
  36. package/dist/schemas/index.js +14 -8
  37. package/dist/{themeLibrary-BIYE6aLw.d.ts → themeLibrary-B2K-aAOy.d.ts} +1 -1
  38. package/dist/timing/index.d.ts +66 -2
  39. package/dist/timing/index.js +12 -0
  40. package/dist/transform/index.d.ts +173 -16
  41. package/dist/transform/index.js +58 -17
  42. package/package.json +1 -1
  43. package/dist/chunk-FCB6JLWB.js +0 -125
@@ -7,6 +7,7 @@ import {
7
7
  detectAsciiDiagram,
8
8
  detectAsciiTimeline,
9
9
  detectTree,
10
+ extractConceptItems,
10
11
  extractEmbeddedVideos,
11
12
  extractHostedVideos,
12
13
  extractImages,
@@ -24,6 +25,7 @@ import {
24
25
  isPictureFence,
25
26
  isPictureParagraph,
26
27
  markdownBlockSeparatorLines,
28
+ matchNumberHighlight,
27
29
  parseAnnotationInner,
28
30
  parseAsciiDiagram,
29
31
  parseAsciiTimeline,
@@ -31,8 +33,12 @@ import {
31
33
  pickAutoTemplate,
32
34
  profileBlockContents,
33
35
  renderMarkdownBlocksHtml,
34
- treeToTemplateData
35
- } from "./chunk-SGNWO7OF.js";
36
+ splitConceptItem,
37
+ splitStatItem,
38
+ timelineItems,
39
+ treeToTemplateData,
40
+ vennOverlapIndex
41
+ } from "./chunk-MA7XUCSQ.js";
36
42
  import {
37
43
  anchorPoint,
38
44
  base64ToUtf8,
@@ -51,6 +57,8 @@ import {
51
57
  createTemplateContext,
52
58
  getTwoColumnPositions,
53
59
  getViewportOrientation,
60
+ hexHueDegrees,
61
+ isHex,
54
62
  isPersistentLayerTemplate,
55
63
  isTemplateBlock,
56
64
  oklchDarken,
@@ -63,11 +71,20 @@ import {
63
71
  validateTheme,
64
72
  withAlpha
65
73
  } from "./chunk-JNUNNQ74.js";
74
+ import {
75
+ taskMarker
76
+ } from "./chunk-4R3YZTPX.js";
77
+ import {
78
+ createMediaTimeMap
79
+ } from "./chunk-MTIR5PWF.js";
80
+ import {
81
+ normalizeMediaCuts
82
+ } from "./chunk-37RLFDIT.js";
66
83
  import {
67
84
  TABLE_FED_TEMPLATES,
68
85
  isContainerTemplate,
69
86
  resolveTemplateName
70
- } from "./chunk-7MS26OXD.js";
87
+ } from "./chunk-UDPPWYYM.js";
71
88
  import {
72
89
  KNOWN_BLOCK_META_KEYS,
73
90
  coerceAnnotationValues,
@@ -629,8 +646,8 @@ var FALLBACK_COLOR_SCHEME = {
629
646
  function resolveColorScheme(context, name) {
630
647
  const schemes = context.theme.colorSchemes;
631
648
  if (name && name in schemes) return schemes[name];
632
- const keys = Object.keys(schemes);
633
- if (keys.length > 0) return schemes[keys[0]];
649
+ const keys2 = Object.keys(schemes);
650
+ if (keys2.length > 0) return schemes[keys2[0]];
634
651
  return FALLBACK_COLOR_SCHEME;
635
652
  }
636
653
  var FALLBACK_BODY = 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
@@ -722,8 +739,8 @@ function getTemplateHint(context, templateName, key, fallback) {
722
739
  // src/doc/templates/captionUtils.ts
723
740
  var TRAILING_PUNCT_RE = /[.;:!?]+$/;
724
741
  var CAPTION_BASELINE_SHIFT_EM = 0.055;
725
- function cleanCaption(text) {
726
- return text.replace(TRAILING_PUNCT_RE, "").trim();
742
+ function cleanCaption(text2) {
743
+ return text2.replace(TRAILING_PUNCT_RE, "").trim();
727
744
  }
728
745
  function createBackgroundLayer(id, fill) {
729
746
  return {
@@ -733,19 +750,19 @@ function createBackgroundLayer(id, fill) {
733
750
  position: { x: 0, y: 0, width: "100%", height: "100%" }
734
751
  };
735
752
  }
736
- function estimateTextHeight(text, fontSizePx, maxWidthPx, lineHeight) {
753
+ function estimateTextHeight(text2, fontSizePx, maxWidthPx, lineHeight) {
737
754
  const avgCharWidth = fontSizePx * 0.52;
738
755
  const charsPerLine = Math.max(8, Math.floor(maxWidthPx / avgCharWidth));
739
- const lines = Math.max(1, Math.ceil(text.length / charsPerLine));
756
+ const lines = Math.max(1, Math.ceil(text2.length / charsPerLine));
740
757
  return lines * fontSizePx * lineHeight;
741
758
  }
742
- function estimateWrappedLineCount(text, fontSize, maxWidth) {
743
- if (!text.trim()) return 1;
759
+ function estimateWrappedLineCount(text2, fontSize, maxWidth) {
760
+ if (!text2.trim()) return 1;
744
761
  const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
745
762
  if (charsPerLine <= 0) return 1;
746
763
  let lineCount = 0;
747
764
  let currentLineLength = 0;
748
- for (const word of text.split(/\s+/)) {
765
+ for (const word of text2.split(/\s+/)) {
749
766
  const testLineLength = currentLineLength ? currentLineLength + 1 + word.length : word.length;
750
767
  if (testLineLength <= charsPerLine) {
751
768
  currentLineLength = testLineLength;
@@ -761,11 +778,11 @@ function estimateWrappedLineCount(text, fontSize, maxWidth) {
761
778
  }
762
779
  return Math.max(1, lineCount + (currentLineLength ? 1 : 0));
763
780
  }
764
- function estimateProseLineCount(text, fontSize, maxWidthPx) {
765
- return text.split("\n").reduce((sum, line) => sum + estimateWrappedLineCount(line, fontSize, maxWidthPx), 0);
781
+ function estimateProseLineCount(text2, fontSize, maxWidthPx) {
782
+ return text2.split("\n").reduce((sum, line) => sum + estimateWrappedLineCount(line, fontSize, maxWidthPx), 0);
766
783
  }
767
784
  function fitProse(options) {
768
- const { text, baseFontSize, minFontSize, maxWidthPx, maxHeightPx, lineHeight } = options;
785
+ const { text: text2, baseFontSize, minFontSize, maxWidthPx, maxHeightPx, lineHeight } = options;
769
786
  const step2 = Math.max(0.5, options.step ?? 2);
770
787
  const floor = Math.min(baseFontSize, minFontSize);
771
788
  const heightAt = (lines, size) => lines * size * lineHeight;
@@ -773,7 +790,7 @@ function fitProse(options) {
773
790
  for (let size = baseFontSize; size > floor; size -= step2) candidates.push(size);
774
791
  candidates.push(floor);
775
792
  for (const size of candidates) {
776
- const lines = estimateProseLineCount(text, size, maxWidthPx);
793
+ const lines = estimateProseLineCount(text2, size, maxWidthPx);
777
794
  const heightPx = heightAt(lines, size);
778
795
  if (heightPx <= maxHeightPx) return { fontSize: size, lines, heightPx };
779
796
  }
@@ -1292,8 +1309,8 @@ function sectionHeader(input, context) {
1292
1309
  // src/doc/utils/textMetrics.ts
1293
1310
  var AVERAGE_CHAR_WIDTH_EM = 0.62;
1294
1311
  var BOLD_CHAR_WIDTH_EM = 0.66;
1295
- function estimateTextWidth(text, fontSize, charWidthEm = AVERAGE_CHAR_WIDTH_EM) {
1296
- return text.length * fontSize * charWidthEm;
1312
+ function estimateTextWidth(text2, fontSize, charWidthEm = AVERAGE_CHAR_WIDTH_EM) {
1313
+ return text2.length * fontSize * charWidthEm;
1297
1314
  }
1298
1315
  var DISPLAY_CHAR_EM = {
1299
1316
  space: 0.27,
@@ -1319,9 +1336,9 @@ function displayCharEm(ch) {
1319
1336
  if (".,:;-/'\u2019!?()".includes(ch)) return DISPLAY_CHAR_EM.punct;
1320
1337
  return DISPLAY_CHAR_EM.other;
1321
1338
  }
1322
- function estimateDisplayTextWidth(text, fontSize) {
1339
+ function estimateDisplayTextWidth(text2, fontSize) {
1323
1340
  let em = 0;
1324
- for (const ch of text.normalize("NFD").replace(/\p{M}/gu, "")) em += displayCharEm(ch);
1341
+ for (const ch of text2.normalize("NFD").replace(/\p{M}/gu, "")) em += displayCharEm(ch);
1325
1342
  return em * fontSize;
1326
1343
  }
1327
1344
 
@@ -1658,8 +1675,8 @@ function collectListItems(block) {
1658
1675
  for (const node of block.contents) {
1659
1676
  walkMarkdownTree(node, (n) => {
1660
1677
  if (n.type === "listItem") {
1661
- const text = extractPlainText(n).replace(/\s+/g, " ").trim();
1662
- if (text) out.push(text);
1678
+ const text2 = extractPlainText(n).replace(/\s+/g, " ").trim();
1679
+ if (text2) out.push(text2);
1663
1680
  return true;
1664
1681
  }
1665
1682
  return;
@@ -1675,14 +1692,14 @@ function getChildrenSummary(block) {
1675
1692
  if (!block.children || block.children.length === 0) return "";
1676
1693
  return block.children.map((c) => c.title?.trim()).filter((t) => Boolean(t)).join(", ");
1677
1694
  }
1678
- function escapeBraces(text) {
1679
- return text.replace(ESCAPE_RE, (_match, inner) => ESCAPED_OPEN + inner + ESCAPED_CLOSE);
1695
+ function escapeBraces(text2) {
1696
+ return text2.replace(ESCAPE_RE, (_match, inner) => ESCAPED_OPEN + inner + ESCAPED_CLOSE);
1680
1697
  }
1681
- function unescapeBraces(text) {
1682
- return text.replace(new RegExp(ESCAPED_OPEN, "g"), "{").replace(new RegExp(ESCAPED_CLOSE, "g"), "}");
1698
+ function unescapeBraces(text2) {
1699
+ return text2.replace(new RegExp(ESCAPED_OPEN, "g"), "{").replace(new RegExp(ESCAPED_CLOSE, "g"), "}");
1683
1700
  }
1684
- function substitute(text, resolve) {
1685
- const escaped = escapeBraces(text);
1701
+ function substitute(text2, resolve) {
1702
+ const escaped = escapeBraces(text2);
1686
1703
  const tokenized = escaped.replace(
1687
1704
  TOKEN_RE,
1688
1705
  (match, name, arg, def) => {
@@ -1725,8 +1742,8 @@ function resolveToken(name, arg, block, images, item) {
1725
1742
  return null;
1726
1743
  }
1727
1744
  }
1728
- function substituteText(text, block, images, item) {
1729
- return substitute(text, (name, arg) => resolveToken(name, arg, block, images, item));
1745
+ function substituteText(text2, block, images, item) {
1746
+ return substitute(text2, (name, arg) => resolveToken(name, arg, block, images, item));
1730
1747
  }
1731
1748
  function resolveImageSrc(src, block, images, item) {
1732
1749
  if (!src.includes("{")) return src;
@@ -1786,7 +1803,7 @@ function collectRepeatItems(source, block, images) {
1786
1803
  index: i + 1
1787
1804
  }));
1788
1805
  case "listItems":
1789
- return collectListItems(block).map((text, i) => ({ text, label: text, index: i + 1 }));
1806
+ return collectListItems(block).map((text2, i) => ({ text: text2, label: text2, index: i + 1 }));
1790
1807
  default:
1791
1808
  return [];
1792
1809
  }
@@ -1873,21 +1890,22 @@ function makeCustomTemplateFn(def) {
1873
1890
 
1874
1891
  // src/doc/utils/countUp.ts
1875
1892
  var STAT_PATTERN = /^([^\d]*?)([-−]?\d[\d,]*(?:\.\d+)?)(.*)$/s;
1876
- function parseStatNumber(text) {
1877
- const match = STAT_PATTERN.exec(text.trim());
1893
+ var MEASURE_UNIT = /^\s?(?:feet|foot|ft|metres?|meters?|m|km|kilomet(?:re|er)s?|miles?|mi|yards?|inch(?:es)?|cm|mm|acres?|hectares?|ha|kg|kilograms?|g|grams?|lbs?|pounds?|tons?|tonnes?|litres?|liters?|gallons?|people|residents|inhabitants|visitors|species|steps|pages|words|°[CF]?|degrees|mph|km\/h|knots|watts?|kW|MW|calories|dollars|euros)\b/i;
1894
+ function parseStatNumber(text2) {
1895
+ const match = STAT_PATTERN.exec(text2.trim());
1878
1896
  if (!match) return null;
1879
1897
  const [, prefix, numeric, suffix] = match;
1880
1898
  const normalized = numeric.replace(/,/g, "").replace("\u2212", "-");
1881
1899
  const value = Number(normalized);
1882
1900
  if (!Number.isFinite(value)) return null;
1883
1901
  const decimals = normalized.includes(".") ? normalized.split(".")[1].length : 0;
1884
- const bare = /^-?\d{4}$/.test(normalized);
1902
+ const bare = /^-?\d{4}$/.test(numeric.replace("\u2212", "-"));
1885
1903
  const looksLikeYear = bare && Math.abs(value) >= 1e3 && Math.abs(value) <= 2999 && !/^[$€£¥]/.test(prefix);
1886
- if (looksLikeYear && !/^[%KMB]/i.test(suffix.trim())) return null;
1904
+ if (looksLikeYear && !/^[%KMB]/i.test(suffix.trim()) && !MEASURE_UNIT.test(suffix)) return null;
1887
1905
  return { prefix, value, decimals, grouping: numeric.includes(","), suffix };
1888
1906
  }
1889
- function countUpSpecFor(text) {
1890
- const parsed = parseStatNumber(text);
1907
+ function countUpSpecFor(text2) {
1908
+ const parsed = parseStatNumber(text2);
1891
1909
  if (!parsed) return null;
1892
1910
  return {
1893
1911
  from: 0,
@@ -1909,13 +1927,13 @@ function formatCountValue(value, decimals, grouping) {
1909
1927
  const sign = value < 0 && Number(fixed) !== 0 ? "-" : "";
1910
1928
  return `${sign}${grouped}${fraction ? `.${fraction}` : ""}`;
1911
1929
  }
1912
- function formatCountUp(text, animation, blockTime) {
1913
- const count = animation.count ?? countUpSpecFor(text);
1914
- if (!count) return text;
1930
+ function formatCountUp(text2, animation, blockTime) {
1931
+ const count = animation.count ?? countUpSpecFor(text2);
1932
+ if (!count) return text2;
1915
1933
  const delay = animation.delay ?? 0;
1916
1934
  const duration = animation.duration ?? 1.5;
1917
1935
  const progress = duration <= 0 ? 1 : Math.min(1, Math.max(0, (blockTime - delay) / duration));
1918
- if (progress >= 1) return animation.count ? formatFinal(count) : text;
1936
+ if (progress >= 1) return animation.count ? formatFinal(count) : text2;
1919
1937
  const from = count.from ?? 0;
1920
1938
  const value = from + (count.to - from) * easeOutCubic(progress);
1921
1939
  return `${count.prefix ?? ""}${formatCountValue(value, count.decimals ?? 0, count.grouping ?? false)}${count.suffix ?? ""}`;
@@ -1924,183 +1942,570 @@ function formatFinal(count) {
1924
1942
  return `${count.prefix ?? ""}${formatCountValue(count.to, count.decimals ?? 0, count.grouping ?? false)}${count.suffix ?? ""}`;
1925
1943
  }
1926
1944
 
1927
- // src/doc/templates/conceptBlocks.ts
1928
- var MAX_ITEMS = 6;
1929
- var LABEL_WORDS = 7;
1930
- function splitConceptItem(text) {
1931
- const clean = text.replace(/\s+/gu, " ").trim();
1932
- const match = clean.match(/^(.{1,64}?)(:\s|\s[—–-]\s)(.+)$/u);
1933
- const range = !!match && !match[2].startsWith(":") && /\d$/u.test(match[1]);
1934
- if (match && !range && !/[.!?]\s/u.test(match[1]) && match[1].split(" ").length <= LABEL_WORDS)
1935
- return { label: match[1].trim(), detail: match[3].trim() };
1936
- return { label: "", detail: clean };
1937
- }
1938
- var HEADLINE_BASE = 60;
1939
- var HEADLINE_MIN = 40;
1940
- var HEADLINE_LINE_HEIGHT = 1.15;
1941
- var LABEL_BASE = 44;
1942
- var DETAIL_BASE = 33;
1943
- var PLAIN_BASE = 38;
1944
- var LABEL_LINE_HEIGHT = 1.2;
1945
- var DETAIL_LINE_HEIGHT = 1.4;
1946
- var MIN_SCALE = 0.62;
1947
- var MAX_SCALE = 1.18;
1948
- var GROW_SHARE = 0.9;
1949
- function ramp(context, scale) {
1945
+ // src/doc/templates/listBlock.ts
1946
+ var LIST_ITEMS_FLOW = "list-items";
1947
+ var LIST_ITEM_LINE_HEIGHT = 1.2;
1948
+ var LIST_ITEM_BASE_FONT_PX = 34;
1949
+ var LIST_ITEM_GAP_PX = 18;
1950
+ var LIST_ITEM_MIN_FONT_PX = 16;
1951
+ var LIST_BOTTOM_MARGIN_PCT = 8;
1952
+ var LIST_STRIP_CLEARANCE_PCT = 2;
1953
+ var LIST_RELAXED_TITLE_TOP_PCT = 20;
1954
+ var LIST_RELAXED_ITEMS_TOP_PCT = 34;
1955
+ var LIST_RELAXED_UNTITLED_TOP_PCT = 26;
1956
+ var LIST_CONDENSED_TOP_PCT = 8;
1957
+ var LIST_CONDENSED_TITLE_GAP_PX = 22;
1958
+ var LIST_TITLE_LINE_HEIGHT = 1.15;
1959
+ function listItemGeometry(fontSize, baseFontSize, itemCount, textWidthPx, ordered = true) {
1960
+ const markerText = ordered ? `${itemCount}.` : "\u2022";
1961
+ const markerWidthPx = Math.max(fontSize, markerText.length * fontSize * 0.5);
1962
+ const markerGapPx = fontSize * 0.35;
1963
+ const bodyIndentPx = markerWidthPx + markerGapPx;
1950
1964
  return {
1951
- label: Math.round(themedFontSize(LABEL_BASE, context, false) * scale),
1952
- detail: Math.round(themedFontSize(DETAIL_BASE, context, false) * scale),
1953
- plain: Math.round(themedFontSize(PLAIN_BASE, context, false) * scale)
1965
+ markerWidthPx,
1966
+ bodyIndentPx,
1967
+ bodyWidthPx: Math.max(fontSize, textWidthPx - bodyIndentPx),
1968
+ gapPx: LIST_ITEM_GAP_PX * (fontSize / baseFontSize)
1954
1969
  };
1955
1970
  }
1956
- function measureItem(item, sizes, width) {
1957
- const labelH = item.label ? estimateProseLineCount(item.label, sizes.label, width) * sizes.label * LABEL_LINE_HEIGHT : 0;
1958
- const size = item.label ? sizes.detail : sizes.plain;
1959
- const detailH = item.detail ? estimateProseLineCount(item.detail, size, width) * size * DETAIL_LINE_HEIGHT : 0;
1960
- const gap = item.label && item.detail ? sizes.label * 0.35 : 0;
1961
- const widest = (text, size2, em) => Math.max(0, ...text.split(/\s+/u).map((word) => word.length)) * size2 * em;
1962
- const overflows = widest(item.label, sizes.label, 0.62) > width || widest(item.detail, size, 0.56) > width;
1963
- return { labelH, gap, detailH, height: overflows ? Infinity : labelH + gap + detailH };
1964
- }
1965
- var SCALES = [MAX_SCALE, 1, 0.82, MIN_SCALE];
1966
- function fitRamp(items, context, width, height) {
1967
- for (const scale of SCALES) {
1968
- const sizes = ramp(context, scale);
1969
- const room = height(scale) * (scale > 1 ? GROW_SHARE : 1);
1970
- if (items.every((item) => measureItem(item, sizes, width(scale)).height <= room))
1971
- return { sizes, scale };
1971
+ function measureListHeightPx(items, fontSize, geometry) {
1972
+ let heightPx = 0;
1973
+ for (const itemText2 of items) {
1974
+ const lineCount = estimateProseLineCount(itemText2, fontSize, geometry.bodyWidthPx);
1975
+ heightPx += lineCount * fontSize * LIST_ITEM_LINE_HEIGHT + geometry.gapPx;
1972
1976
  }
1973
- return { sizes: ramp(context, MIN_SCALE), scale: MIN_SCALE };
1974
- }
1975
- function accentColor(input, context) {
1976
- return input.colorScheme ? resolveColorScheme(context, input.colorScheme).accent : context.theme.colors.highlight || context.theme.colors.primary;
1977
+ return heightPx;
1977
1978
  }
1978
- function frame(input, context) {
1979
- const { theme, viewport } = context;
1980
- const { width: W, height: H } = viewport;
1981
- const portrait = H > W;
1982
- const accent = accentColor(input, context);
1983
- const margin = Math.round(W * (portrait ? 0.08 : 0.07));
1984
- const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
1985
- const title = (input.title ?? "").trim();
1986
- let top = H * 0.1;
1987
- if (title) {
1988
- const headlineTop = H * (portrait ? 0.08 : 0.11);
1989
- const base = themedFontSize(HEADLINE_BASE, context, true);
1990
- const fit = fitProse({
1991
- text: title,
1992
- baseFontSize: base,
1993
- minFontSize: Math.min(base, themedFontSize(HEADLINE_MIN, context, true)),
1994
- maxWidthPx: W - margin * 2,
1995
- maxHeightPx: base * HEADLINE_LINE_HEIGHT * 2,
1996
- lineHeight: HEADLINE_LINE_HEIGHT
1997
- });
1998
- const ruleH = Math.max(4, Math.round(H * 6e-3));
1999
- layers.push({
1979
+ function listBlock(input, context) {
1980
+ const { title } = input;
1981
+ const items = Array.isArray(input.items) ? input.items : [];
1982
+ if (items.length === 0 && !context.block?.summaryMode) return [];
1983
+ const build = input.build ?? context.block?.build;
1984
+ if (build && build.mode !== "off") {
1985
+ const rich = extractRichListItems(context.block?.contents);
1986
+ const matches = rich.length === items.length && rich.every((item, i) => item.text === items[i]);
1987
+ const contents = matches ? context.block?.contents ?? [] : [
1988
+ {
1989
+ type: "list",
1990
+ ordered: input.ordered !== false,
1991
+ children: items.map((text2) => ({
1992
+ type: "listItem",
1993
+ children: [{ type: "paragraph", children: [{ type: "text", value: text2 }] }]
1994
+ }))
1995
+ }
1996
+ ];
1997
+ const layers2 = contentBlock(
1998
+ { ...input, template: "content", title: input.title ?? "", build },
1999
+ {
2000
+ ...context,
2001
+ block: {
2002
+ id: input.id,
2003
+ duration: input.duration,
2004
+ startTime: 0,
2005
+ audioSegment: input.audioSegment,
2006
+ ...context.block,
2007
+ summaryMode: void 0,
2008
+ contents,
2009
+ build
2010
+ }
2011
+ }
2012
+ );
2013
+ const accent = accentForViewport(input.accentImage, context.viewport);
2014
+ if (accent) {
2015
+ const layout = getAccentLayout(accent.position);
2016
+ layers2.splice(
2017
+ 1,
2018
+ 0,
2019
+ ...createAccentLayers(
2020
+ accent,
2021
+ input.id,
2022
+ themedImageTreatment(context, input.imageTreatment)
2023
+ )
2024
+ );
2025
+ for (const layer of layers2) {
2026
+ if (layer.type !== "text" || layer.id !== "title" && layer.id !== "body") continue;
2027
+ layer.position.x = `${parseFloat(layout.textCenterX) - parseFloat(layout.textWidth) / 2}%`;
2028
+ layer.position.width = layout.textWidth;
2029
+ layer.position.y = adjustY(String(layer.position.y), layout);
2030
+ if (layer.id === "body" && accent.position === "bottom-strip")
2031
+ layer.position.height = `${72 - ACCENT_STRIP_SIZE}%`;
2032
+ }
2033
+ }
2034
+ return layers2;
2035
+ }
2036
+ const accentImage = accentForViewport(input.accentImage, context.viewport);
2037
+ const ordered = input.ordered !== false;
2038
+ const richItems = extractRichListItems(context.block?.contents);
2039
+ const { theme } = context;
2040
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
2041
+ const titleFontSize = themedFontSize(44, context, true);
2042
+ const baseItemFontSize = themedFontSize(LIST_ITEM_BASE_FONT_PX, context, false);
2043
+ const minItemFontSize = Math.min(
2044
+ baseItemFontSize,
2045
+ themedFontSize(LIST_ITEM_MIN_FONT_PX, context, false)
2046
+ );
2047
+ const layers = [
2048
+ // Background — gradient
2049
+ {
2000
2050
  type: "shape",
2001
- id: "accent-rule",
2002
- position: {
2003
- x: margin,
2004
- y: headlineTop - ruleH - fit.fontSize * 0.55,
2005
- width: W * 0.045,
2006
- height: ruleH
2051
+ id: "bg",
2052
+ content: {
2053
+ shape: "rect",
2054
+ fill: `linear-gradient(155deg, ${theme.colors.backgroundLight} 0%, ${theme.colors.background} 100%)`
2007
2055
  },
2008
- content: { shape: "rect", fill: accent, borderRadius: ruleH / 2 },
2009
- animation: entrance(context, "rule", 0)
2010
- });
2011
- layers.push({
2012
- ...textLayer("title", title, box(margin, headlineTop, W - margin * 2, fit.heightPx), {
2013
- fontSize: fit.fontSize,
2014
- fontFamily: getThemeFont(context, "title"),
2015
- fontWeight: "bold",
2016
- color: theme.colors.text,
2017
- lineHeight: HEADLINE_LINE_HEIGHT,
2018
- shadow: shouldUseShadow(context),
2019
- ...fit.maxLines ? { maxLines: fit.maxLines } : {}
2020
- }),
2021
- animation: entrance(context, "text", 0.1)
2022
- });
2023
- top = headlineTop + fit.heightPx + H * (portrait ? 0.05 : 0.07);
2024
- }
2025
- return {
2026
- layers,
2027
- accent,
2028
- left: margin,
2029
- right: W - margin,
2030
- top,
2031
- bottom: H * (portrait ? 0.92 : 0.9),
2032
- portrait
2033
- };
2034
- }
2035
- function box(left, top, width, height) {
2036
- return { x: left + width / 2, y: top + height / 2, width, height, anchor: "center" };
2037
- }
2038
- function textLayer(id, text, position, style) {
2039
- return {
2040
- type: "text",
2041
- id,
2042
- position,
2043
- content: {
2044
- text,
2045
- style: { textAlign: "left", verticalAlign: "top", shrinkToFit: true, ...style }
2056
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2046
2057
  }
2047
- };
2048
- }
2049
- function itemText(id, item, measure, sizes, left, top, width, align, context) {
2050
- const { theme } = context;
2051
- const slack = (size) => size * 0.4;
2052
- const layers = [];
2053
- if (item.label)
2054
- layers.push(
2055
- textLayer(
2056
- `${id}-label`,
2057
- item.label,
2058
- box(left, top, width, measure.labelH + slack(sizes.label)),
2059
- {
2060
- fontSize: sizes.label,
2061
- fontFamily: getThemeFont(context, "title"),
2062
- fontWeight: "bold",
2063
- color: theme.colors.text,
2064
- lineHeight: LABEL_LINE_HEIGHT,
2065
- textAlign: align
2066
- }
2067
- )
2068
- );
2069
- if (item.detail) {
2070
- const size = item.label ? sizes.detail : sizes.plain;
2058
+ ];
2059
+ if (accentImage) {
2071
2060
  layers.push(
2072
- textLayer(
2073
- `${id}-detail`,
2074
- item.detail,
2075
- box(left, top + measure.labelH + measure.gap, width, measure.detailH + slack(size)),
2076
- {
2077
- fontSize: size,
2078
- fontFamily: getThemeFont(context, "body"),
2079
- // Details read as secondary by weight and a little transparency, not a
2080
- // muted hue that loses contrast on dark themes.
2081
- color: item.label ? withAlpha(theme.colors.text, 0.8) : theme.colors.text,
2082
- lineHeight: DETAIL_LINE_HEIGHT,
2083
- textAlign: align
2084
- }
2061
+ ...createAccentLayers(
2062
+ accentImage,
2063
+ input.id,
2064
+ themedImageTreatment(context, input.imageTreatment)
2085
2065
  )
2086
2066
  );
2087
2067
  }
2088
- return layers;
2089
- }
2090
- function itemsOf(input) {
2091
- return (Array.isArray(input.items) ? input.items : []).map((item) => String(item ?? "").trim()).filter(Boolean).slice(0, MAX_ITEMS).map(splitConceptItem);
2092
- }
2093
- function conceptBuildStepTexts(template, items) {
2094
- const shown = itemsOf({ items: [...items] });
2095
- const dated = template === "process" ? timelineItems(shown) : void 0;
2096
- if (dated)
2068
+ const centerX = parseFloat(accentLayout.textCenterX);
2069
+ const widthPct = parseFloat(accentLayout.textWidth);
2070
+ const leftPct = Number.isFinite(centerX) && Number.isFinite(widthPct) ? centerX - widthPct / 2 : 8;
2071
+ const leftX = `${leftPct}%`;
2072
+ const textWidthPx = (Number.isFinite(widthPct) ? widthPct / 100 : 0.85) * context.viewport.width;
2073
+ const bottomLimitPct = (accentImage?.position === "bottom-strip" ? 100 - ACCENT_STRIP_SIZE - LIST_STRIP_CLEARANCE_PCT : 100 - LIST_BOTTOM_MARGIN_PCT) - accentLayout.textYAdjust;
2074
+ const baseGeometry = listItemGeometry(
2075
+ baseItemFontSize,
2076
+ baseItemFontSize,
2077
+ items.length,
2078
+ textWidthPx,
2079
+ ordered
2080
+ );
2081
+ const relaxedStartY = title ? LIST_RELAXED_ITEMS_TOP_PCT : LIST_RELAXED_UNTITLED_TOP_PCT;
2082
+ const relaxedBottomPct = relaxedStartY + measureListHeightPx(items, baseItemFontSize, baseGeometry) / context.viewport.height * 100;
2083
+ const condensed = relaxedBottomPct > bottomLimitPct;
2084
+ const condensedTopPct = LIST_CONDENSED_TOP_PCT - accentLayout.textYAdjust;
2085
+ const titleTopPct = condensed ? condensedTopPct : LIST_RELAXED_TITLE_TOP_PCT;
2086
+ let startY = relaxedStartY;
2087
+ if (condensed) {
2088
+ const titleHeightPx = title ? estimateWrappedLineCount(title, titleFontSize, textWidthPx) * titleFontSize * LIST_TITLE_LINE_HEIGHT + LIST_CONDENSED_TITLE_GAP_PX : 0;
2089
+ startY = condensedTopPct + titleHeightPx / context.viewport.height * 100;
2090
+ }
2091
+ if (title) {
2092
+ layers.push({
2093
+ type: "text",
2094
+ id: "list-title",
2095
+ content: {
2096
+ text: title,
2097
+ style: {
2098
+ fontSize: titleFontSize,
2099
+ fontFamily: getThemeFont(context, "title"),
2100
+ fontWeight: "bold",
2101
+ color: theme.colors.text,
2102
+ textAlign: "left",
2103
+ shadow: shouldUseShadow(context)
2104
+ }
2105
+ },
2106
+ position: {
2107
+ x: leftX,
2108
+ y: adjustY(`${titleTopPct}%`, accentLayout),
2109
+ width: accentLayout.textWidth
2110
+ },
2111
+ animation: motionEntrance(context, { type: "fadeIn", duration: 1 })
2112
+ });
2113
+ }
2114
+ const availablePx = Math.max(0, (bottomLimitPct - startY) / 100 * context.viewport.height);
2115
+ let itemFontSize = baseItemFontSize;
2116
+ let geometry = baseGeometry;
2117
+ while (itemFontSize > minItemFontSize && measureListHeightPx(items, itemFontSize, geometry) > availablePx) {
2118
+ itemFontSize -= 1;
2119
+ geometry = listItemGeometry(itemFontSize, baseItemFontSize, items.length, textWidthPx, ordered);
2120
+ }
2121
+ const { markerWidthPx, bodyIndentPx, bodyWidthPx, gapPx } = geometry;
2122
+ const bodyLeftX = `${leftPct + bodyIndentPx / context.viewport.width * 100}%`;
2123
+ let itemY = startY;
2124
+ for (let i = 0; i < items.length; i++) {
2125
+ const itemText2 = items[i];
2126
+ const itemHtml = richItems[i]?.text === itemText2 ? richItems[i]?.html : void 0;
2127
+ const lineCount = estimateProseLineCount(itemText2, itemFontSize, bodyWidthPx);
2128
+ const animation = motionEntrance(context, {
2129
+ type: "fadeIn",
2130
+ duration: 0.8,
2131
+ delay: 0.3 + 0.3 * i
2132
+ });
2133
+ layers.push({
2134
+ type: "text",
2135
+ id: `item-${i}-marker`,
2136
+ content: {
2137
+ // Bullets stay bullets; only ordered (or legacy, unflagged) lists count.
2138
+ text: ordered ? `${i + 1}.` : "\u2022",
2139
+ flow: { id: LIST_ITEMS_FLOW, role: "marker" },
2140
+ style: {
2141
+ fontSize: itemFontSize,
2142
+ fontFamily: getThemeFont(context, "body"),
2143
+ color: theme.colors.text,
2144
+ textAlign: "right",
2145
+ lineHeight: LIST_ITEM_LINE_HEIGHT,
2146
+ shadow: shouldUseShadow(context)
2147
+ }
2148
+ },
2149
+ position: {
2150
+ x: leftX,
2151
+ y: adjustY(`${itemY}%`, accentLayout),
2152
+ width: markerWidthPx
2153
+ },
2154
+ animation
2155
+ });
2156
+ layers.push({
2157
+ type: "text",
2158
+ id: `item-${i}`,
2159
+ content: {
2160
+ text: itemText2,
2161
+ ...itemHtml ? { html: itemHtml } : {},
2162
+ paragraphs: [
2163
+ {
2164
+ text: itemText2,
2165
+ list: { ordered, level: 0, ...ordered ? { number: i + 1 } : {} }
2166
+ }
2167
+ ],
2168
+ flow: { id: LIST_ITEMS_FLOW },
2169
+ style: {
2170
+ fontSize: itemFontSize,
2171
+ fontFamily: getThemeFont(context, "body"),
2172
+ color: theme.colors.text,
2173
+ textAlign: "left",
2174
+ lineHeight: LIST_ITEM_LINE_HEIGHT,
2175
+ shadow: shouldUseShadow(context)
2176
+ }
2177
+ },
2178
+ position: {
2179
+ x: bodyLeftX,
2180
+ y: adjustY(`${itemY}%`, accentLayout),
2181
+ width: bodyWidthPx,
2182
+ // Rich (HTML) items render in a hidden-overflow box the renderer
2183
+ // sizes from the plain text; size it here from the same line count
2184
+ // that reserved the vertical space so nested sub-items are not cut off.
2185
+ height: lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT + itemFontSize * 0.5
2186
+ },
2187
+ animation
2188
+ });
2189
+ const itemHeightPx = lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT;
2190
+ itemY += (itemHeightPx + gapPx) / context.viewport.height * 100;
2191
+ }
2192
+ return layers;
2193
+ }
2194
+
2195
+ // src/doc/templates/conceptSpecs.ts
2196
+ var CONCEPT_TEMPLATES = [
2197
+ "cards",
2198
+ "process",
2199
+ "cycle",
2200
+ "layers",
2201
+ "stats",
2202
+ "pyramid",
2203
+ "funnel",
2204
+ "hub",
2205
+ "venn",
2206
+ "matrix"
2207
+ ];
2208
+ var CONCEPT_MAX_DRAWN = 6;
2209
+ var CYCLE_STAGE_CHARACTERS = 170;
2210
+ var CONCEPT_SPECS = {
2211
+ cards: {
2212
+ items: [2, 4],
2213
+ itemForm: "label-detail",
2214
+ inputs: [{ key: "versus", guide: "versus: true for exactly two set against each other" }]
2215
+ },
2216
+ process: { items: [2, 4], itemForm: "label-detail", order: "in order" },
2217
+ cycle: { items: [3, 5], itemForm: "label-detail" },
2218
+ layers: { items: [3, 5], itemForm: "label-detail", order: "innermost first" },
2219
+ stats: { items: [2, 4], itemForm: "figure-caption" },
2220
+ pyramid: { items: [3, 5], itemForm: "label-detail", order: "from the top down" },
2221
+ funnel: { items: [3, 5], itemForm: "label-detail", order: "widest first" },
2222
+ hub: {
2223
+ items: [3, 6],
2224
+ itemForm: "label-detail",
2225
+ inputs: [{ key: "center", guide: "center \u2264 3 words" }]
2226
+ },
2227
+ // Two sets and the item that says what they share.
2228
+ venn: { items: [3, 3], itemForm: "label-detail" },
2229
+ matrix: {
2230
+ items: [4, 4],
2231
+ itemForm: "label-detail",
2232
+ order: "in reading order (top left, top right, bottom left, bottom right)",
2233
+ inputs: [
2234
+ { key: "xAxis", guide: "xAxis" },
2235
+ { key: "yAxis", guide: "yAxis" }
2236
+ ]
2237
+ }
2238
+ };
2239
+ function conceptTemplate(template) {
2240
+ return CONCEPT_TEMPLATES.find((name) => name === template);
2241
+ }
2242
+ function range(template, options) {
2243
+ const [min, most] = CONCEPT_SPECS[template].items;
2244
+ return [min, Math.max(min, Math.min(most, options.max ?? most))];
2245
+ }
2246
+ function conceptItemCount(template, options = {}) {
2247
+ const [min, max] = range(template, options);
2248
+ return min === max ? `exactly ${min}` : `${min}\u2013${max}`;
2249
+ }
2250
+ function itemTexts(items) {
2251
+ return (Array.isArray(items) ? items : []).map(
2252
+ (item) => String(item ?? "").replace(/\s+/gu, " ").trim()
2253
+ ).filter(Boolean);
2254
+ }
2255
+ var quoted = (text2) => `"${text2.length > 48 ? `${text2.slice(0, 47)}\u2026` : text2}"`;
2256
+ function conceptItemsProblem(template, items, options = {}) {
2257
+ const texts = itemTexts(items);
2258
+ const [min, max] = range(template, options);
2259
+ if (texts.length < min || texts.length > max)
2260
+ return `${template} takes ${conceptItemCount(template, options)} items and this has ${texts.length}`;
2261
+ if (template === "stats") {
2262
+ const bare = texts.find((text2) => !splitStatItem(text2).figure);
2263
+ if (bare !== void 0)
2264
+ return `every stats item is "figure: what it measures" and ${quoted(bare)} states no figure`;
2265
+ }
2266
+ if (template === "venn" && vennOverlapIndex(texts.map(splitConceptItem)) < 0)
2267
+ return 'venn needs one item labelled "Both:" that says what the two share';
2268
+ return void 0;
2269
+ }
2270
+ function cycleDrawsAsProcess(items) {
2271
+ return items.length < 3 || items.some((item) => item.detail.length > CYCLE_STAGE_CHARACTERS);
2272
+ }
2273
+ function conceptDowngrade(template, items) {
2274
+ const texts = itemTexts(items);
2275
+ if (texts.length > CONCEPT_MAX_DRAWN)
2276
+ return {
2277
+ drawsAs: "list",
2278
+ reason: `a drawing holds ${CONCEPT_MAX_DRAWN} items and this has ${texts.length}`
2279
+ };
2280
+ const parsed = texts.map(splitConceptItem);
2281
+ const n = parsed.length;
2282
+ switch (template) {
2283
+ case "cycle":
2284
+ if (n < 3)
2285
+ return {
2286
+ drawsAs: "process",
2287
+ reason: `a ring needs at least three stages and this has ${n}`
2288
+ };
2289
+ return cycleDrawsAsProcess(parsed) ? { drawsAs: "process", reason: "a stage runs too long to sit beside a ring" } : void 0;
2290
+ case "layers":
2291
+ case "pyramid":
2292
+ case "funnel":
2293
+ case "hub":
2294
+ return n < 2 ? { drawsAs: "cards", reason: `it needs at least two items and this has ${n}` } : void 0;
2295
+ case "stats":
2296
+ return texts.some((text2) => splitStatItem(text2).figure) ? void 0 : { drawsAs: "cards", reason: "no item states a figure" };
2297
+ case "venn": {
2298
+ const sets = n - (vennOverlapIndex(parsed) >= 0 ? 1 : 0);
2299
+ return sets === 2 ? void 0 : {
2300
+ drawsAs: "cards",
2301
+ reason: `a Venn diagram draws two sets (and one item labelled "Both:") and this has ${sets}`
2302
+ };
2303
+ }
2304
+ case "matrix":
2305
+ return n === 4 ? void 0 : { drawsAs: "cards", reason: `a matrix places exactly four items and this has ${n}` };
2306
+ default:
2307
+ return void 0;
2308
+ }
2309
+ }
2310
+ function conceptDrawsAs(template, items) {
2311
+ return conceptDowngrade(template, items)?.drawsAs ?? template;
2312
+ }
2313
+
2314
+ // src/doc/templates/conceptBlocks.ts
2315
+ var MAX_ITEMS = CONCEPT_MAX_DRAWN;
2316
+ var HEADLINE_BASE = 60;
2317
+ var HEADLINE_MIN = 40;
2318
+ var HEADLINE_LINE_HEIGHT = 1.15;
2319
+ var LABEL_BASE = 44;
2320
+ var DETAIL_BASE = 33;
2321
+ var PLAIN_BASE = 38;
2322
+ var LABEL_LINE_HEIGHT = 1.2;
2323
+ var DETAIL_LINE_HEIGHT = 1.4;
2324
+ var MIN_SCALE = 0.62;
2325
+ var MAX_SCALE = 1.18;
2326
+ var GROW_SHARE = 0.9;
2327
+ function ramp(context, scale) {
2328
+ return {
2329
+ label: Math.round(themedFontSize(LABEL_BASE, context, false) * scale),
2330
+ detail: Math.round(themedFontSize(DETAIL_BASE, context, false) * scale),
2331
+ plain: Math.round(themedFontSize(PLAIN_BASE, context, false) * scale)
2332
+ };
2333
+ }
2334
+ function measureItem(item, sizes, width) {
2335
+ const labelH = item.label ? estimateProseLineCount(item.label, sizes.label, width) * sizes.label * LABEL_LINE_HEIGHT : 0;
2336
+ const size = item.label ? sizes.detail : sizes.plain;
2337
+ const detailH = item.detail ? estimateProseLineCount(item.detail, size, width) * size * DETAIL_LINE_HEIGHT : 0;
2338
+ const gap = item.label && item.detail ? sizes.label * 0.35 : 0;
2339
+ const widest = (text2, size2, em) => Math.max(0, ...text2.split(/\s+/u).map((word) => word.length)) * size2 * em;
2340
+ const overflows = widest(item.label, sizes.label, 0.62) > width || widest(item.detail, size, 0.56) > width;
2341
+ return { labelH, gap, detailH, height: overflows ? Infinity : labelH + gap + detailH };
2342
+ }
2343
+ var SCALES = [MAX_SCALE, 1, 0.82, MIN_SCALE];
2344
+ function fitRamp(items, context, width, height) {
2345
+ for (const scale of SCALES) {
2346
+ const sizes = ramp(context, scale);
2347
+ const room = height(scale) * (scale > 1 ? GROW_SHARE : 1);
2348
+ if (items.every((item) => measureItem(item, sizes, width(scale)).height <= room))
2349
+ return { sizes, scale };
2350
+ }
2351
+ return { sizes: ramp(context, MIN_SCALE), scale: MIN_SCALE };
2352
+ }
2353
+ function accentColor(input, context) {
2354
+ return input.colorScheme ? resolveColorScheme(context, input.colorScheme).accent : context.theme.colors.highlight || context.theme.colors.primary;
2355
+ }
2356
+ function frame(input, context) {
2357
+ const { theme, viewport } = context;
2358
+ const { width: W, height: H } = viewport;
2359
+ const portrait = H > W;
2360
+ const accent = accentColor(input, context);
2361
+ const margin = Math.round(W * (portrait ? 0.08 : 0.07));
2362
+ const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
2363
+ const title = (input.title ?? "").trim();
2364
+ let top = H * 0.1;
2365
+ if (title) {
2366
+ const headlineTop = H * (portrait ? 0.08 : 0.11);
2367
+ const base = themedFontSize(HEADLINE_BASE, context, true);
2368
+ const fit = fitProse({
2369
+ text: title,
2370
+ baseFontSize: base,
2371
+ minFontSize: Math.min(base, themedFontSize(HEADLINE_MIN, context, true)),
2372
+ maxWidthPx: W - margin * 2,
2373
+ maxHeightPx: base * HEADLINE_LINE_HEIGHT * 2,
2374
+ lineHeight: HEADLINE_LINE_HEIGHT
2375
+ });
2376
+ const ruleH = Math.max(4, Math.round(H * 6e-3));
2377
+ layers.push({
2378
+ type: "shape",
2379
+ id: "accent-rule",
2380
+ position: {
2381
+ x: margin,
2382
+ y: headlineTop - ruleH - fit.fontSize * 0.55,
2383
+ width: W * 0.045,
2384
+ height: ruleH
2385
+ },
2386
+ content: { shape: "rect", fill: accent, borderRadius: ruleH / 2 },
2387
+ animation: entrance(context, "rule", 0)
2388
+ });
2389
+ layers.push({
2390
+ ...textLayer("title", title, box(margin, headlineTop, W - margin * 2, fit.heightPx), {
2391
+ fontSize: fit.fontSize,
2392
+ fontFamily: getThemeFont(context, "title"),
2393
+ fontWeight: "bold",
2394
+ color: theme.colors.text,
2395
+ lineHeight: HEADLINE_LINE_HEIGHT,
2396
+ shadow: shouldUseShadow(context),
2397
+ ...fit.maxLines ? { maxLines: fit.maxLines } : {}
2398
+ }),
2399
+ animation: entrance(context, "text", 0.1)
2400
+ });
2401
+ top = headlineTop + fit.heightPx + H * (portrait ? 0.05 : 0.07);
2402
+ }
2403
+ return {
2404
+ layers,
2405
+ accent,
2406
+ left: margin,
2407
+ right: W - margin,
2408
+ top,
2409
+ bottom: H * (portrait ? 0.92 : 0.9),
2410
+ portrait
2411
+ };
2412
+ }
2413
+ function box(left, top, width, height) {
2414
+ return { x: left + width / 2, y: top + height / 2, width, height, anchor: "center" };
2415
+ }
2416
+ function textLayer(id, text2, position, style) {
2417
+ return {
2418
+ type: "text",
2419
+ id,
2420
+ position,
2421
+ content: {
2422
+ text: text2,
2423
+ style: { textAlign: "left", verticalAlign: "top", shrinkToFit: true, ...style }
2424
+ }
2425
+ };
2426
+ }
2427
+ function itemText(id, item, measure, sizes, left, top, width, align, context) {
2428
+ const { theme } = context;
2429
+ const slack = (size) => size * 0.4;
2430
+ const layers = [];
2431
+ if (item.label)
2432
+ layers.push(
2433
+ textLayer(
2434
+ `${id}-label`,
2435
+ item.label,
2436
+ box(left, top, width, measure.labelH + slack(sizes.label)),
2437
+ {
2438
+ fontSize: sizes.label,
2439
+ fontFamily: getThemeFont(context, "title"),
2440
+ fontWeight: "bold",
2441
+ color: theme.colors.text,
2442
+ lineHeight: LABEL_LINE_HEIGHT,
2443
+ textAlign: align
2444
+ }
2445
+ )
2446
+ );
2447
+ if (item.detail) {
2448
+ const size = item.label ? sizes.detail : sizes.plain;
2449
+ layers.push(
2450
+ textLayer(
2451
+ `${id}-detail`,
2452
+ item.detail,
2453
+ box(left, top + measure.labelH + measure.gap, width, measure.detailH + slack(size)),
2454
+ {
2455
+ fontSize: size,
2456
+ fontFamily: getThemeFont(context, "body"),
2457
+ // Details read as secondary by weight and a little transparency, not a
2458
+ // muted hue that loses contrast on dark themes.
2459
+ color: item.label ? withAlpha(theme.colors.text, 0.8) : theme.colors.text,
2460
+ lineHeight: DETAIL_LINE_HEIGHT,
2461
+ textAlign: align
2462
+ }
2463
+ )
2464
+ );
2465
+ }
2466
+ return layers;
2467
+ }
2468
+ function itemTexts2(input) {
2469
+ return (Array.isArray(input.items) ? input.items : []).map((item) => String(item ?? "").trim()).filter(Boolean).slice(0, MAX_ITEMS);
2470
+ }
2471
+ function itemsOf(input) {
2472
+ return itemTexts2(input).map(splitConceptItem);
2473
+ }
2474
+ function withoutIdeas(area, context) {
2475
+ return context.block?.contents?.length ? [] : area.layers;
2476
+ }
2477
+ function asListWhenCrowded(input, context) {
2478
+ const items = (Array.isArray(input.items) ? input.items : []).map((item) => String(item ?? "").trim()).filter(Boolean);
2479
+ if (items.length <= MAX_ITEMS) return void 0;
2480
+ return listBlock(
2481
+ {
2482
+ ...input,
2483
+ template: "list",
2484
+ items,
2485
+ ordered: input.template === "process"
2486
+ },
2487
+ context
2488
+ );
2489
+ }
2490
+ function statItemsOf(input) {
2491
+ return itemTexts2(input).map(splitStatItem);
2492
+ }
2493
+ function conceptBuildStepTexts(template, items) {
2494
+ const shown = itemsOf({ items: [...items] });
2495
+ const stats = template === "stats" ? statItemsOf({ items: [...items] }) : [];
2496
+ if (stats.some((stat) => stat.figure))
2497
+ return stats.map((stat) => {
2498
+ const { number, unit } = splitFigure(stat.figure);
2499
+ return [number, unit, stat.caption].filter(Boolean).join(" ");
2500
+ });
2501
+ if (template !== "process" && template !== "cycle")
2502
+ return shown.map((item) => [item.label, item.detail].filter(Boolean).join(" "));
2503
+ const asProcess = template === "process" || cycleDrawsAsProcess(shown);
2504
+ const dated = asProcess ? timelineItems(shown) : void 0;
2505
+ if (dated)
2097
2506
  return dated.map((item) => [item.when, item.label, item.detail].filter(Boolean).join(" "));
2098
2507
  return shown.map(
2099
- (item, index) => [
2100
- template === "cards" || template === "layers" ? "" : String(index + 1),
2101
- item.label,
2102
- item.detail
2103
- ].filter(Boolean).join(" ")
2508
+ (item, index) => [String(index + 1), item.label, item.detail].filter(Boolean).join(" ")
2104
2509
  );
2105
2510
  }
2106
2511
  var FIRST_TURN = 0.6;
@@ -2149,18 +2554,18 @@ function words(layers, at) {
2149
2554
  var LEAD_BASE = 44;
2150
2555
  var LEAD_MIN = 28;
2151
2556
  var LEAD_LINE_HEIGHT = 1.4;
2152
- function lead(area, text, context) {
2557
+ function lead(area, text2, context) {
2153
2558
  const { theme, viewport } = context;
2154
2559
  const barW = Math.max(5, Math.round(viewport.width * 4e-3));
2155
2560
  const indent = barW + Math.round(viewport.width * 0.018);
2156
2561
  const width = (area.right - area.left) * (area.portrait ? 1 : 0.8) - indent;
2157
2562
  const base = themedFontSize(
2158
- text.length <= 90 ? LEAD_BASE * 1.3 : text.length <= 160 ? LEAD_BASE * 1.15 : LEAD_BASE,
2563
+ text2.length <= 90 ? LEAD_BASE * 1.3 : text2.length <= 160 ? LEAD_BASE * 1.15 : LEAD_BASE,
2159
2564
  context,
2160
2565
  false
2161
2566
  );
2162
2567
  const fit = fitProse({
2163
- text,
2568
+ text: text2,
2164
2569
  baseFontSize: base,
2165
2570
  minFontSize: Math.min(base, themedFontSize(LEAD_MIN, context, false)),
2166
2571
  maxWidthPx: width,
@@ -2190,7 +2595,7 @@ function lead(area, text, context) {
2190
2595
  [
2191
2596
  textLayer(
2192
2597
  "lead",
2193
- text,
2598
+ text2,
2194
2599
  box(area.left + indent, top, width, fit.heightPx + fit.fontSize * 0.4),
2195
2600
  {
2196
2601
  fontSize: fit.fontSize,
@@ -2212,13 +2617,13 @@ function lead(area, text, context) {
2212
2617
  }
2213
2618
  var STATEMENT_BASE = 84;
2214
2619
  var STATEMENT_MIN = 48;
2215
- function statement(area, text, context) {
2620
+ function statement(area, text2, context) {
2216
2621
  const { theme, viewport } = context;
2217
2622
  const W = area.right - area.left;
2218
2623
  const H = area.bottom - area.top;
2219
2624
  const base = themedFontSize(STATEMENT_BASE, context, true);
2220
2625
  const fit = fitProse({
2221
- text,
2626
+ text: text2,
2222
2627
  baseFontSize: base,
2223
2628
  minFontSize: Math.min(base, themedFontSize(STATEMENT_MIN, context, true)),
2224
2629
  maxWidthPx: W * (area.portrait ? 1 : 0.86),
@@ -2249,7 +2654,7 @@ function statement(area, text, context) {
2249
2654
  [
2250
2655
  textLayer(
2251
2656
  "statement",
2252
- text,
2657
+ text2,
2253
2658
  box(area.left, top, W * (area.portrait ? 1 : 0.86), fit.heightPx + fit.fontSize * 0.3),
2254
2659
  {
2255
2660
  fontSize: fit.fontSize,
@@ -2271,10 +2676,12 @@ function statement(area, text, context) {
2271
2676
  ];
2272
2677
  }
2273
2678
  function cardsBlock(input, context) {
2679
+ const crowded = asListWhenCrowded(input, context);
2680
+ if (crowded) return crowded;
2274
2681
  const items = itemsOf(input);
2275
2682
  const area = frame(input, context);
2276
2683
  const { theme, viewport } = context;
2277
- if (!items.length) return area.layers;
2684
+ if (!items.length) return withoutIdeas(area, context);
2278
2685
  if (items.length === 1 && !items[0].label)
2279
2686
  return (input.title?.trim() ? lead : statement)(area, items[0].detail, context);
2280
2687
  const contrast = String(input.versus) === "true";
@@ -2506,10 +2913,12 @@ function connector(id, d, color, width, arrow = true) {
2506
2913
  };
2507
2914
  }
2508
2915
  function processBlock(input, context) {
2916
+ const crowded = asListWhenCrowded(input, context);
2917
+ if (crowded) return crowded;
2509
2918
  const items = itemsOf(input);
2510
2919
  const area = frame(input, context);
2511
2920
  const { viewport } = context;
2512
- if (!items.length) return area.layers;
2921
+ if (!items.length) return withoutIdeas(area, context);
2513
2922
  const dated = timelineItems(items);
2514
2923
  if (dated) return timeline(area, dated, context);
2515
2924
  const W = area.right - area.left;
@@ -2638,30 +3047,6 @@ function processBlock(input, context) {
2638
3047
  });
2639
3048
  return layers;
2640
3049
  }
2641
- var MONTH = "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\\.?\\s+";
2642
- var LEADING_DATE = new RegExp(
2643
- `^((?:${MONTH}(?:\\d{1,2},?\\s+)?)?(?:\\d{4}s?|\\d{1,4}\\s?(?:BCE?|AD|CE))(?![.,]?\\d))[\\s,:;\u2014\u2013-]*(.*)$`,
2644
- "u"
2645
- );
2646
- function timelineItems(items) {
2647
- if (items.length < 2) return void 0;
2648
- let last = -Infinity;
2649
- const dated = [];
2650
- for (const item of items) {
2651
- const match = (item.label || item.detail).match(LEADING_DATE);
2652
- if (!match) return void 0;
2653
- const when = match[1];
2654
- const year = Number(when.match(/\d{1,4}(?=s?\s?(?:BCE?|AD|CE)?$)/u)?.[0] ?? NaN);
2655
- const signed = /BCE?$/u.test(when) ? -year : year;
2656
- if (!Number.isFinite(signed) || signed < last) return void 0;
2657
- last = signed;
2658
- const rest = match[2].trim();
2659
- dated.push(
2660
- item.label ? { when, label: rest, detail: item.detail } : { when, label: "", detail: rest }
2661
- );
2662
- }
2663
- return dated;
2664
- }
2665
3050
  function timeline(area, items, context) {
2666
3051
  const { viewport } = context;
2667
3052
  const W = area.right - area.left;
@@ -2824,10 +3209,12 @@ function timeline(area, items, context) {
2824
3209
  return layers;
2825
3210
  }
2826
3211
  function cycleBlock(input, context) {
3212
+ const crowded = asListWhenCrowded(input, context);
3213
+ if (crowded) return crowded;
2827
3214
  const items = itemsOf(input);
2828
3215
  const area = frame(input, context);
2829
3216
  const { viewport } = context;
2830
- if (items.length < 2) return processBlock({ ...input, template: "process" }, context);
3217
+ if (cycleDrawsAsProcess(items)) return processBlock({ ...input, template: "process" }, context);
2831
3218
  const H = area.bottom - area.top;
2832
3219
  const n = items.length;
2833
3220
  const cx = (area.left + area.right) / 2;
@@ -2912,8 +3299,11 @@ function cycleBlock(input, context) {
2912
3299
  return layers;
2913
3300
  }
2914
3301
  function layersBlock(input, context) {
3302
+ const crowded = asListWhenCrowded(input, context);
3303
+ if (crowded) return crowded;
2915
3304
  const items = itemsOf(input);
2916
- if (items.length < 2) return cardsBlock({ ...input, template: "cards" }, context);
3305
+ if (conceptDrawsAs("layers", input.items) === "cards")
3306
+ return cardsBlock({ ...input, template: "cards" }, context);
2917
3307
  const area = frame(input, context);
2918
3308
  const { theme, viewport } = context;
2919
3309
  const W = area.right - area.left;
@@ -4241,10 +4631,10 @@ function estimateLineEm(line, mono) {
4241
4631
  }
4242
4632
  return em;
4243
4633
  }
4244
- function simulateRendererLines(text, fontSize, boxWidthPx) {
4634
+ function simulateRendererLines(text2, fontSize, boxWidthPx) {
4245
4635
  const charsPerLine = Math.floor(boxWidthPx / (fontSize * RENDERER_CHAR_EM));
4246
4636
  const lines = [];
4247
- for (const segment of text.split("\n")) {
4637
+ for (const segment of text2.split("\n")) {
4248
4638
  if (!segment.trim() || charsPerLine <= 0) {
4249
4639
  lines.push(segment);
4250
4640
  continue;
@@ -4268,14 +4658,14 @@ function simulateRendererLines(text, fontSize, boxWidthPx) {
4268
4658
  }
4269
4659
  return lines;
4270
4660
  }
4271
- function fitWrapWidth(text, fontSize, columnWidthPx, fontFamily) {
4661
+ function fitWrapWidth(text2, fontSize, columnWidthPx, fontFamily) {
4272
4662
  const mono = isMonoFamily(fontFamily);
4273
- if (!mono && !WIDE_GLYPH_RE.test(text)) return columnWidthPx;
4663
+ if (!mono && !WIDE_GLYPH_RE.test(text2)) return columnWidthPx;
4274
4664
  const floor = columnWidthPx * 0.5;
4275
4665
  const step2 = columnWidthPx * 0.02;
4276
4666
  let width = columnWidthPx;
4277
4667
  while (width > floor) {
4278
- const lines = simulateRendererLines(text, fontSize, width);
4668
+ const lines = simulateRendererLines(text2, fontSize, width);
4279
4669
  const widest = Math.max(...lines.map((line) => estimateLineEm(line, mono))) * fontSize;
4280
4670
  if (widest <= columnWidthPx) return width;
4281
4671
  width -= step2;
@@ -4542,328 +4932,903 @@ var LINE_HEIGHT = 1.2;
4542
4932
  var FULL_SIZE_CHARS = 40;
4543
4933
  var MIN_SIZE_CHARS = 160;
4544
4934
  var MIN_LENGTH_FACTOR = 0.6;
4545
- function lengthFactor(text) {
4546
- const length = text.trim().length;
4935
+ function lengthFactor(text2) {
4936
+ const length = text2.trim().length;
4547
4937
  if (length <= FULL_SIZE_CHARS) return 1;
4548
4938
  const t = Math.min(1, (length - FULL_SIZE_CHARS) / (MIN_SIZE_CHARS - FULL_SIZE_CHARS));
4549
4939
  return 1 - t * (1 - MIN_LENGTH_FACTOR);
4550
4940
  }
4551
4941
  function fullBleedQuote(input, context) {
4552
- const { text, colorScheme = "blue" } = input;
4942
+ const { text: text2, colorScheme = "blue" } = input;
4553
4943
  const { viewport } = context;
4554
4944
  const colors = resolveColorScheme(context, colorScheme);
4555
4945
  const H = viewport.height;
4556
4946
  const W = viewport.width;
4557
4947
  const pct2 = (px) => `${Number((px / H * 100).toFixed(3))}%`;
4558
4948
  const impactFontSize = themedFontSize(120, context, true);
4559
- const baseFontSize = Math.round(impactFontSize * lengthFactor(text));
4949
+ const baseFontSize = Math.round(impactFontSize * lengthFactor(text2));
4560
4950
  const bandHeightPx = (BAND_BOTTOM3 - BAND_TOP3) * H;
4561
4951
  const fit = fitProse({
4562
- text,
4952
+ text: text2,
4563
4953
  baseFontSize,
4564
4954
  minFontSize: Math.max(28, impactFontSize / 3),
4565
4955
  maxWidthPx: COLUMN_WIDTH * W,
4566
- maxHeightPx: bandHeightPx * BAND_FILL,
4567
- lineHeight: LINE_HEIGHT
4568
- });
4569
- const topPx = Math.max(
4570
- BAND_TOP3 * H,
4571
- (BAND_TOP3 + (BAND_BOTTOM3 - BAND_TOP3) / 2) * H - fit.heightPx / 2
4572
- );
4573
- return [
4574
- // Background — radial vignette built from the color scheme's own
4575
- // surface, so the card takes on the scheme (and the theme behind it)
4576
- // instead of vignetting every theme into hard black.
4577
- {
4578
- type: "shape",
4579
- id: "bg",
4580
- content: {
4581
- shape: "rect",
4582
- fill: `radial-gradient(ellipse at 50% 50%, ${colors.bg} 0%, ${oklchDarken(colors.bg, 0.12)} 100%)`
4583
- },
4584
- position: { x: 0, y: 0, width: "100%", height: "100%" }
4585
- },
4586
- // The text — massive, centered, boxed so it can never leave the frame.
4587
- {
4588
- type: "text",
4589
- id: "impact-text",
4590
- content: {
4591
- text,
4592
- style: {
4593
- fontSize: fit.fontSize,
4594
- fontFamily: getThemeFont(context, "title"),
4595
- fontWeight: "bold",
4596
- color: colors.text,
4597
- textAlign: "center",
4598
- lineHeight: LINE_HEIGHT,
4599
- shadow: shouldUseShadow(context),
4600
- // Real glyphs of wide display / mono / CJK faces exceed the 0.5em
4601
- // wrap estimate; the renderer measures and scales into the box.
4602
- shrinkToFit: true,
4603
- ...fit.maxLines ? { maxLines: fit.maxLines } : {}
4604
- }
4605
- },
4606
- position: {
4607
- x: `${COLUMN_LEFT * 100}%`,
4608
- y: pct2(topPx),
4609
- width: `${COLUMN_WIDTH * 100}%`,
4610
- height: pct2(fit.heightPx),
4611
- anchor: "top-left"
4612
- },
4613
- animation: motionEntrance(
4614
- context,
4615
- getTemplateHint(context, "fullBleedQuote", "entrance", "subtle") === "dramatic" ? { type: "zoomIn", duration: 0.8 } : { type: "fadeIn", duration: 1.5 }
4616
- )
4617
- }
4618
- ];
4619
- }
4620
-
4621
- // src/doc/templates/listBlock.ts
4622
- var LIST_ITEMS_FLOW = "list-items";
4623
- var LIST_ITEM_LINE_HEIGHT = 1.2;
4624
- var LIST_ITEM_BASE_FONT_PX = 34;
4625
- var LIST_ITEM_GAP_PX = 18;
4626
- var LIST_ITEM_MIN_FONT_PX = 16;
4627
- var LIST_BOTTOM_MARGIN_PCT = 8;
4628
- var LIST_STRIP_CLEARANCE_PCT = 2;
4629
- var LIST_RELAXED_TITLE_TOP_PCT = 20;
4630
- var LIST_RELAXED_ITEMS_TOP_PCT = 34;
4631
- var LIST_RELAXED_UNTITLED_TOP_PCT = 26;
4632
- var LIST_CONDENSED_TOP_PCT = 8;
4633
- var LIST_CONDENSED_TITLE_GAP_PX = 22;
4634
- var LIST_TITLE_LINE_HEIGHT = 1.15;
4635
- function listItemGeometry(fontSize, baseFontSize, itemCount, textWidthPx, ordered = true) {
4636
- const markerText = ordered ? `${itemCount}.` : "\u2022";
4637
- const markerWidthPx = Math.max(fontSize, markerText.length * fontSize * 0.5);
4638
- const markerGapPx = fontSize * 0.35;
4639
- const bodyIndentPx = markerWidthPx + markerGapPx;
4640
- return {
4641
- markerWidthPx,
4642
- bodyIndentPx,
4643
- bodyWidthPx: Math.max(fontSize, textWidthPx - bodyIndentPx),
4644
- gapPx: LIST_ITEM_GAP_PX * (fontSize / baseFontSize)
4645
- };
4646
- }
4647
- function measureListHeightPx(items, fontSize, geometry) {
4648
- let heightPx = 0;
4649
- for (const itemText2 of items) {
4650
- const lineCount = estimateProseLineCount(itemText2, fontSize, geometry.bodyWidthPx);
4651
- heightPx += lineCount * fontSize * LIST_ITEM_LINE_HEIGHT + geometry.gapPx;
4652
- }
4653
- return heightPx;
4654
- }
4655
- function listBlock(input, context) {
4656
- const { title } = input;
4657
- const items = Array.isArray(input.items) ? input.items : [];
4658
- const build = input.build ?? context.block?.build;
4659
- if (build && build.mode !== "off") {
4660
- const rich = extractRichListItems(context.block?.contents);
4661
- const matches = rich.length === items.length && rich.every((item, i) => item.text === items[i]);
4662
- const contents = matches ? context.block?.contents ?? [] : [
4663
- {
4664
- type: "list",
4665
- ordered: input.ordered !== false,
4666
- children: items.map((text) => ({
4667
- type: "listItem",
4668
- children: [{ type: "paragraph", children: [{ type: "text", value: text }] }]
4669
- }))
4670
- }
4671
- ];
4672
- const layers2 = contentBlock(
4673
- { ...input, template: "content", title: input.title ?? "", build },
4674
- {
4675
- ...context,
4676
- block: {
4677
- id: input.id,
4678
- duration: input.duration,
4679
- startTime: 0,
4680
- audioSegment: input.audioSegment,
4681
- ...context.block,
4682
- summaryMode: void 0,
4683
- contents,
4684
- build
4685
- }
4686
- }
4687
- );
4688
- const accent = accentForViewport(input.accentImage, context.viewport);
4689
- if (accent) {
4690
- const layout = getAccentLayout(accent.position);
4691
- layers2.splice(
4692
- 1,
4693
- 0,
4694
- ...createAccentLayers(
4695
- accent,
4696
- input.id,
4697
- themedImageTreatment(context, input.imageTreatment)
4698
- )
4699
- );
4700
- for (const layer of layers2) {
4701
- if (layer.type !== "text" || layer.id !== "title" && layer.id !== "body") continue;
4702
- layer.position.x = `${parseFloat(layout.textCenterX) - parseFloat(layout.textWidth) / 2}%`;
4703
- layer.position.width = layout.textWidth;
4704
- layer.position.y = adjustY(String(layer.position.y), layout);
4705
- if (layer.id === "body" && accent.position === "bottom-strip")
4706
- layer.position.height = `${72 - ACCENT_STRIP_SIZE}%`;
4707
- }
4708
- }
4709
- return layers2;
4710
- }
4711
- const accentImage = accentForViewport(input.accentImage, context.viewport);
4712
- const ordered = input.ordered !== false;
4713
- const richItems = extractRichListItems(context.block?.contents);
4714
- const { theme } = context;
4715
- const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
4716
- const titleFontSize = themedFontSize(44, context, true);
4717
- const baseItemFontSize = themedFontSize(LIST_ITEM_BASE_FONT_PX, context, false);
4718
- const minItemFontSize = Math.min(
4719
- baseItemFontSize,
4720
- themedFontSize(LIST_ITEM_MIN_FONT_PX, context, false)
4721
- );
4722
- const layers = [
4723
- // Background — gradient
4724
- {
4725
- type: "shape",
4726
- id: "bg",
4727
- content: {
4728
- shape: "rect",
4729
- fill: `linear-gradient(155deg, ${theme.colors.backgroundLight} 0%, ${theme.colors.background} 100%)`
4730
- },
4731
- position: { x: 0, y: 0, width: "100%", height: "100%" }
4732
- }
4733
- ];
4734
- if (accentImage) {
4735
- layers.push(
4736
- ...createAccentLayers(
4737
- accentImage,
4738
- input.id,
4739
- themedImageTreatment(context, input.imageTreatment)
4740
- )
4741
- );
4742
- }
4743
- const centerX = parseFloat(accentLayout.textCenterX);
4744
- const widthPct = parseFloat(accentLayout.textWidth);
4745
- const leftPct = Number.isFinite(centerX) && Number.isFinite(widthPct) ? centerX - widthPct / 2 : 8;
4746
- const leftX = `${leftPct}%`;
4747
- const textWidthPx = (Number.isFinite(widthPct) ? widthPct / 100 : 0.85) * context.viewport.width;
4748
- const bottomLimitPct = (accentImage?.position === "bottom-strip" ? 100 - ACCENT_STRIP_SIZE - LIST_STRIP_CLEARANCE_PCT : 100 - LIST_BOTTOM_MARGIN_PCT) - accentLayout.textYAdjust;
4749
- const baseGeometry = listItemGeometry(
4750
- baseItemFontSize,
4751
- baseItemFontSize,
4752
- items.length,
4753
- textWidthPx,
4754
- ordered
4956
+ maxHeightPx: bandHeightPx * BAND_FILL,
4957
+ lineHeight: LINE_HEIGHT
4958
+ });
4959
+ const topPx = Math.max(
4960
+ BAND_TOP3 * H,
4961
+ (BAND_TOP3 + (BAND_BOTTOM3 - BAND_TOP3) / 2) * H - fit.heightPx / 2
4755
4962
  );
4756
- const relaxedStartY = title ? LIST_RELAXED_ITEMS_TOP_PCT : LIST_RELAXED_UNTITLED_TOP_PCT;
4757
- const relaxedBottomPct = relaxedStartY + measureListHeightPx(items, baseItemFontSize, baseGeometry) / context.viewport.height * 100;
4758
- const condensed = relaxedBottomPct > bottomLimitPct;
4759
- const condensedTopPct = LIST_CONDENSED_TOP_PCT - accentLayout.textYAdjust;
4760
- const titleTopPct = condensed ? condensedTopPct : LIST_RELAXED_TITLE_TOP_PCT;
4761
- let startY = relaxedStartY;
4762
- if (condensed) {
4763
- const titleHeightPx = title ? estimateWrappedLineCount(title, titleFontSize, textWidthPx) * titleFontSize * LIST_TITLE_LINE_HEIGHT + LIST_CONDENSED_TITLE_GAP_PX : 0;
4764
- startY = condensedTopPct + titleHeightPx / context.viewport.height * 100;
4765
- }
4766
- if (title) {
4767
- layers.push({
4963
+ return [
4964
+ // Background — radial vignette built from the color scheme's own
4965
+ // surface, so the card takes on the scheme (and the theme behind it)
4966
+ // instead of vignetting every theme into hard black.
4967
+ {
4968
+ type: "shape",
4969
+ id: "bg",
4970
+ content: {
4971
+ shape: "rect",
4972
+ fill: `radial-gradient(ellipse at 50% 50%, ${colors.bg} 0%, ${oklchDarken(colors.bg, 0.12)} 100%)`
4973
+ },
4974
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
4975
+ },
4976
+ // The text — massive, centered, boxed so it can never leave the frame.
4977
+ {
4768
4978
  type: "text",
4769
- id: "list-title",
4979
+ id: "impact-text",
4770
4980
  content: {
4771
- text: title,
4981
+ text: text2,
4772
4982
  style: {
4773
- fontSize: titleFontSize,
4983
+ fontSize: fit.fontSize,
4774
4984
  fontFamily: getThemeFont(context, "title"),
4775
4985
  fontWeight: "bold",
4776
- color: theme.colors.text,
4777
- textAlign: "left",
4778
- shadow: shouldUseShadow(context)
4986
+ color: colors.text,
4987
+ textAlign: "center",
4988
+ lineHeight: LINE_HEIGHT,
4989
+ shadow: shouldUseShadow(context),
4990
+ // Real glyphs of wide display / mono / CJK faces exceed the 0.5em
4991
+ // wrap estimate; the renderer measures and scales into the box.
4992
+ shrinkToFit: true,
4993
+ ...fit.maxLines ? { maxLines: fit.maxLines } : {}
4779
4994
  }
4780
4995
  },
4781
4996
  position: {
4782
- x: leftX,
4783
- y: adjustY(`${titleTopPct}%`, accentLayout),
4784
- width: accentLayout.textWidth
4997
+ x: `${COLUMN_LEFT * 100}%`,
4998
+ y: pct2(topPx),
4999
+ width: `${COLUMN_WIDTH * 100}%`,
5000
+ height: pct2(fit.heightPx),
5001
+ anchor: "top-left"
4785
5002
  },
4786
- animation: motionEntrance(context, { type: "fadeIn", duration: 1 })
4787
- });
4788
- }
4789
- const availablePx = Math.max(0, (bottomLimitPct - startY) / 100 * context.viewport.height);
4790
- let itemFontSize = baseItemFontSize;
4791
- let geometry = baseGeometry;
4792
- while (itemFontSize > minItemFontSize && measureListHeightPx(items, itemFontSize, geometry) > availablePx) {
4793
- itemFontSize -= 1;
4794
- geometry = listItemGeometry(itemFontSize, baseItemFontSize, items.length, textWidthPx, ordered);
4795
- }
4796
- const { markerWidthPx, bodyIndentPx, bodyWidthPx, gapPx } = geometry;
4797
- const bodyLeftX = `${leftPct + bodyIndentPx / context.viewport.width * 100}%`;
4798
- let itemY = startY;
4799
- for (let i = 0; i < items.length; i++) {
4800
- const itemText2 = items[i];
4801
- const itemHtml = richItems[i]?.text === itemText2 ? richItems[i]?.html : void 0;
4802
- const lineCount = estimateProseLineCount(itemText2, itemFontSize, bodyWidthPx);
4803
- const animation = motionEntrance(context, {
4804
- type: "fadeIn",
4805
- duration: 0.8,
4806
- delay: 0.3 + 0.3 * i
4807
- });
4808
- layers.push({
4809
- type: "text",
4810
- id: `item-${i}-marker`,
4811
- content: {
4812
- // Bullets stay bullets; only ordered (or legacy, unflagged) lists count.
4813
- text: ordered ? `${i + 1}.` : "\u2022",
4814
- flow: { id: LIST_ITEMS_FLOW, role: "marker" },
4815
- style: {
4816
- fontSize: itemFontSize,
4817
- fontFamily: getThemeFont(context, "body"),
4818
- color: theme.colors.text,
4819
- textAlign: "right",
4820
- lineHeight: LIST_ITEM_LINE_HEIGHT,
4821
- shadow: shouldUseShadow(context)
5003
+ animation: motionEntrance(
5004
+ context,
5005
+ getTemplateHint(context, "fullBleedQuote", "entrance", "subtle") === "dramatic" ? { type: "zoomIn", duration: 0.8 } : { type: "fadeIn", duration: 1.5 }
5006
+ )
5007
+ }
5008
+ ];
5009
+ }
5010
+
5011
+ // src/doc/templates/conceptShapes.ts
5012
+ function region(id, d, fill, stroke, strokeWidth) {
5013
+ return {
5014
+ type: "path",
5015
+ id,
5016
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
5017
+ content: { d, fill, stroke, strokeWidth }
5018
+ };
5019
+ }
5020
+ function disc(id, cx, cy, r, fill) {
5021
+ return {
5022
+ type: "shape",
5023
+ id,
5024
+ position: { x: cx - r, y: cy - r, width: r * 2, height: r * 2 },
5025
+ content: { shape: "circle", fill }
5026
+ };
5027
+ }
5028
+ function unbrokenSize(text2, size, width, floor) {
5029
+ const longest = Math.max(1, ...text2.split(/\s+/u).map((word) => word.length));
5030
+ return Math.max(Math.min(size, floor), Math.min(size, Math.floor(width / (longest * 0.64))));
5031
+ }
5032
+ function panel(id, left, top, width, height, pad, ruleH, accent, context) {
5033
+ const { theme } = context;
5034
+ return [
5035
+ [
5036
+ {
5037
+ type: "shape",
5038
+ id,
5039
+ position: { x: left, y: top, width, height },
5040
+ content: {
5041
+ shape: "rect",
5042
+ fill: withAlpha(theme.colors.text, 0.06),
5043
+ stroke: withAlpha(theme.colors.text, 0.12),
5044
+ strokeWidth: 2,
5045
+ borderRadius: Math.round(Math.min(width, height) * 0.06)
4822
5046
  }
4823
5047
  },
4824
- position: {
4825
- x: leftX,
4826
- y: adjustY(`${itemY}%`, accentLayout),
4827
- width: markerWidthPx
5048
+ "surface",
5049
+ 0
5050
+ ],
5051
+ [
5052
+ {
5053
+ type: "shape",
5054
+ id: `${id}-rule`,
5055
+ position: { x: left + pad, y: top + pad, width: Math.min(width * 0.22, 64), height: ruleH },
5056
+ content: { shape: "rect", fill: accent, borderRadius: ruleH / 2 }
4828
5057
  },
4829
- animation
5058
+ "rule",
5059
+ 0.2
5060
+ ]
5061
+ ];
5062
+ }
5063
+ var FIGURE_LINE_HEIGHT2 = 1.05;
5064
+ function statsBlock(input, context) {
5065
+ const crowded = asListWhenCrowded(input, context);
5066
+ if (crowded) return crowded;
5067
+ const stats = statItemsOf(input);
5068
+ if (conceptDrawsAs("stats", input.items) === "cards")
5069
+ return cardsBlock({ ...input, template: "cards" }, context);
5070
+ const area = frame(input, context);
5071
+ const { theme, viewport } = context;
5072
+ const W = area.right - area.left;
5073
+ const H = area.bottom - area.top;
5074
+ const n = stats.length;
5075
+ const columns2 = area.portrait ? n <= 3 ? 1 : 2 : n <= 4 ? n : 3;
5076
+ const rows = Math.ceil(n / columns2);
5077
+ const gutter = Math.round(viewport.width * (area.portrait ? 0.07 : 0.045));
5078
+ const rowGap = Math.round(viewport.height * 0.055);
5079
+ const cellW = (W - gutter * (columns2 - 1)) / columns2;
5080
+ const cellH = (H - rowGap * (rows - 1)) / rows;
5081
+ const figures = stats.map((stat) => splitFigure(stat.figure));
5082
+ const base = themedFontSize(
5083
+ rows > 1 && !area.portrait ? 124 : columns2 >= 4 ? 132 : columns2 === 3 ? 156 : 176,
5084
+ context,
5085
+ true
5086
+ );
5087
+ const floor = Math.min(base, themedFontSize(64, context, true));
5088
+ const figureSize = Math.min(
5089
+ ...figures.map(
5090
+ ({ number }) => number ? fitProse({
5091
+ text: number,
5092
+ baseFontSize: base,
5093
+ minFontSize: floor,
5094
+ maxWidthPx: cellW * 0.98,
5095
+ maxHeightPx: base * FIGURE_LINE_HEIGHT2,
5096
+ lineHeight: FIGURE_LINE_HEIGHT2
5097
+ }).fontSize : base
5098
+ )
5099
+ );
5100
+ const figureH = figureSize * FIGURE_LINE_HEIGHT2;
5101
+ const unitSize = Math.round(themedFontSize(LABEL_BASE, context, true) * (figureSize / base));
5102
+ const unitH = figures.some((figure) => figure.unit) ? unitSize * LABEL_LINE_HEIGHT : 0;
5103
+ const captionGap = Math.round(figureSize * 0.16);
5104
+ const captions = stats.map((stat) => ({ label: "", detail: stat.caption }));
5105
+ const { sizes } = fitRamp(
5106
+ captions,
5107
+ context,
5108
+ () => cellW,
5109
+ () => Math.max(0, cellH - figureH - unitH - captionGap)
5110
+ );
5111
+ const measures = captions.map((caption) => measureItem(caption, sizes, cellW));
5112
+ const captionH = Math.max(0, ...measures.map((m) => Number.isFinite(m.height) ? m.height : 0));
5113
+ const groupH = Math.min(cellH, figureH + unitH + captionGap + captionH);
5114
+ const gridH = rows * groupH + (rows - 1) * rowGap;
5115
+ const gridTop = area.top + Math.max(0, (H - gridH) * 0.38);
5116
+ const motion = getMotionProfile(context);
5117
+ const ruleW = Math.max(2, Math.round(viewport.width * 12e-4));
5118
+ const layers = [...area.layers];
5119
+ stats.forEach((stat, index) => {
5120
+ const row = Math.floor(index / columns2);
5121
+ const column = index % columns2;
5122
+ const inRow = Math.min(columns2, n - row * columns2);
5123
+ const rowLeft = area.left + (columns2 - inRow) * (cellW + gutter) / 2;
5124
+ const left = rowLeft + column * (cellW + gutter);
5125
+ const top = gridTop + row * (groupH + rowGap);
5126
+ const turn = FIRST_TURN + index * TURN_STAGGER;
5127
+ const { number, unit } = figures[index];
5128
+ const parts = [];
5129
+ if (column > 0)
5130
+ parts.push([
5131
+ {
5132
+ type: "shape",
5133
+ id: `stat-${index}-divider`,
5134
+ position: { x: left - gutter / 2 - ruleW / 2, y: top, width: ruleW, height: groupH },
5135
+ content: { shape: "rect", fill: withAlpha(theme.colors.text, 0.18) }
5136
+ },
5137
+ "bar",
5138
+ 0
5139
+ ]);
5140
+ const built = step(parts, index, context);
5141
+ if (number) {
5142
+ const count = motion.countUp ? countUpSpecFor(number) : null;
5143
+ built.push({
5144
+ ...textLayer(`stat-${index}`, number, box(left, top, cellW, figureH), {
5145
+ fontSize: figureSize,
5146
+ fontFamily: getThemeFont(context, "title"),
5147
+ fontWeight: "bold",
5148
+ color: area.accent,
5149
+ lineHeight: FIGURE_LINE_HEIGHT2,
5150
+ shadow: shouldUseShadow(context)
5151
+ }),
5152
+ buildStep: index,
5153
+ animation: count ? {
5154
+ type: "countUp",
5155
+ duration: motion.countUpDuration,
5156
+ delay: motionDelay(context, turn + 0.1),
5157
+ count
5158
+ } : entrance(context, "pop", turn + 0.1)
5159
+ });
5160
+ }
5161
+ const rest = [];
5162
+ if (unit)
5163
+ rest.push([
5164
+ textLayer(`stat-${index}-unit`, unit, box(left, top + figureH, cellW, unitH), {
5165
+ fontSize: unitSize,
5166
+ fontFamily: getThemeFont(context, "title"),
5167
+ fontWeight: "bold",
5168
+ color: area.accent,
5169
+ lineHeight: LABEL_LINE_HEIGHT
5170
+ }),
5171
+ "text",
5172
+ 0.25
5173
+ ]);
5174
+ rest.push(
5175
+ ...words(
5176
+ itemText(
5177
+ `stat-${index}`,
5178
+ captions[index],
5179
+ measures[index],
5180
+ sizes,
5181
+ left,
5182
+ top + (number ? figureH + unitH + captionGap : 0),
5183
+ cellW,
5184
+ "left",
5185
+ context
5186
+ ),
5187
+ 0.35
5188
+ )
5189
+ );
5190
+ layers.push(...built, ...step(rest, index, context));
5191
+ });
5192
+ return layers;
5193
+ }
5194
+ var FUNNEL_STEM = 0.3;
5195
+ function tiers(kind, input, context) {
5196
+ const crowded = asListWhenCrowded(input, context);
5197
+ if (crowded) return crowded;
5198
+ const items = itemsOf(input);
5199
+ if (conceptDrawsAs(kind, input.items) === "cards")
5200
+ return cardsBlock({ ...input, template: "cards" }, context);
5201
+ const area = frame(input, context);
5202
+ const { theme, viewport } = context;
5203
+ const W = area.right - area.left;
5204
+ const H = area.bottom - area.top;
5205
+ const n = items.length;
5206
+ const portrait = area.portrait;
5207
+ const gap = Math.round(viewport.width * (portrait ? 0.02 : 0.045));
5208
+ const rowGap = Math.round(viewport.height * 0.02);
5209
+ const listGap = viewport.height * 0.045;
5210
+ const top = area.top;
5211
+ const listW = W - gap * 1.5;
5212
+ const listed = portrait ? fitRamp(
5213
+ items,
5214
+ context,
5215
+ () => listW,
5216
+ () => (H * 0.64 - listGap - rowGap * (n - 1)) / n
5217
+ ).sizes : void 0;
5218
+ const listH = listed ? items.reduce(
5219
+ (sum, item) => {
5220
+ const height = measureItem(item, listed, listW).height;
5221
+ return sum + (Number.isFinite(height) ? height : 0);
5222
+ },
5223
+ rowGap * (n - 1)
5224
+ ) : 0;
5225
+ const shapeH = Math.round(
5226
+ portrait ? Math.max(H * 0.36, Math.min(H * 0.5, H - listH - listGap)) : H
5227
+ );
5228
+ const shapeW = Math.round(
5229
+ portrait ? Math.min(W * 0.78, shapeH * 1.5) : Math.min(W * 0.4, H * 1.1)
5230
+ );
5231
+ const shapeLeft = portrait ? area.left + (W - shapeW) / 2 : area.left;
5232
+ const cx = shapeLeft + shapeW / 2;
5233
+ const slit = Math.max(3, Math.round(viewport.height * 7e-3));
5234
+ const tierH = shapeH / n;
5235
+ const half = (y) => kind === "pyramid" ? shapeW / 2 * (y / shapeH) : shapeW / 2 * (1 - (1 - FUNNEL_STEM) * (y / shapeH));
5236
+ const tint = (index) => {
5237
+ const depth = n > 1 ? index / (n - 1) : 0;
5238
+ return withAlpha(area.accent, kind === "pyramid" ? 0.92 - 0.6 * depth : 0.32 + 0.6 * depth);
5239
+ };
5240
+ const textLeft = portrait ? area.left + gap * 1.5 : shapeLeft + shapeW + gap;
5241
+ const textW = area.right - textLeft;
5242
+ const textTop = portrait ? top + shapeH + listGap : top;
5243
+ const sizes = listed ?? fitRamp(
5244
+ items,
5245
+ context,
5246
+ () => textW,
5247
+ () => tierH - rowGap
5248
+ ).sizes;
5249
+ const measures = items.map((item) => measureItem(item, sizes, textW));
5250
+ const stroke = Math.max(2, Math.round(viewport.height * 3e-3));
5251
+ const shapes = [];
5252
+ const callouts = [];
5253
+ let rowTop = textTop;
5254
+ items.forEach((item, index) => {
5255
+ const y0 = index * tierH;
5256
+ const y1 = y0 + tierH - (index < n - 1 ? slit : 0);
5257
+ const [a, b] = [half(y0), half(y1)];
5258
+ shapes.push(
5259
+ ...step(
5260
+ [
5261
+ [
5262
+ region(
5263
+ `tier-${index}`,
5264
+ `M ${cx - a} ${top + y0} L ${cx + a} ${top + y0} L ${cx + b} ${top + y1} L ${cx - b} ${top + y1} Z`,
5265
+ tint(index),
5266
+ "none",
5267
+ 0
5268
+ ),
5269
+ "surface",
5270
+ 0
5271
+ ]
5272
+ ],
5273
+ index,
5274
+ context
5275
+ )
5276
+ );
5277
+ const m = measures[index];
5278
+ const height = Number.isFinite(m.height) ? m.height : tierH;
5279
+ const middle = top + (y0 + y1) / 2;
5280
+ const parts = [];
5281
+ let blockTop;
5282
+ if (portrait) {
5283
+ const firstLine = item.label ? sizes.label * LABEL_LINE_HEIGHT : sizes.plain * DETAIL_LINE_HEIGHT;
5284
+ const swatch = Math.round(sizes.label * 0.55);
5285
+ parts.push([
5286
+ {
5287
+ type: "shape",
5288
+ id: `tier-${index}-swatch`,
5289
+ position: {
5290
+ x: area.left,
5291
+ y: rowTop + firstLine / 2 - swatch / 2,
5292
+ width: swatch,
5293
+ height: swatch
5294
+ },
5295
+ content: { shape: "rect", fill: tint(index), borderRadius: Math.round(swatch * 0.2) }
5296
+ },
5297
+ "pop",
5298
+ 0.2
5299
+ ]);
5300
+ blockTop = rowTop;
5301
+ rowTop += height + rowGap;
5302
+ } else {
5303
+ const edge = cx + half((y0 + y1) / 2);
5304
+ const dot = Math.max(8, Math.round(viewport.height * 9e-3));
5305
+ parts.push(
5306
+ [
5307
+ connector(
5308
+ `tier-${index}-callout`,
5309
+ `M ${edge + dot} ${middle} L ${textLeft - gap * 0.25} ${middle}`,
5310
+ withAlpha(theme.colors.text, 0.45),
5311
+ stroke,
5312
+ false
5313
+ ),
5314
+ "draw",
5315
+ 0.2
5316
+ ],
5317
+ [
5318
+ disc(`tier-${index}-pin`, edge + dot / 2, middle, dot / 2, theme.colors.text),
5319
+ "pop",
5320
+ 0.15
5321
+ ]
5322
+ );
5323
+ blockTop = Math.max(area.top, middle - height / 2);
5324
+ }
5325
+ parts.push(
5326
+ ...words(
5327
+ itemText(`tier-${index}`, item, m, sizes, textLeft, blockTop, textW, "left", context),
5328
+ 0.35
5329
+ )
5330
+ );
5331
+ callouts.push(...step(parts, index, context));
5332
+ });
5333
+ return [...area.layers, ...shapes, ...callouts];
5334
+ }
5335
+ function pyramidBlock(input, context) {
5336
+ return tiers("pyramid", input, context);
5337
+ }
5338
+ function funnelBlock(input, context) {
5339
+ return tiers("funnel", input, context);
5340
+ }
5341
+ function hubBlock(input, context) {
5342
+ const crowded = asListWhenCrowded(input, context);
5343
+ if (crowded) return crowded;
5344
+ const items = itemsOf(input);
5345
+ if (conceptDrawsAs("hub", input.items) === "cards")
5346
+ return cardsBlock({ ...input, template: "cards" }, context);
5347
+ const area = frame(input, context);
5348
+ const { viewport } = context;
5349
+ const W = area.right - area.left;
5350
+ const H = area.bottom - area.top;
5351
+ const n = items.length;
5352
+ const portrait = area.portrait;
5353
+ const center = String(input.center ?? "").replace(/\s+/gu, " ").trim();
5354
+ const hubR = Math.round(
5355
+ Math.min(viewport.height, viewport.width) * (center ? portrait ? 0.09 : 0.115 : 0.055)
5356
+ );
5357
+ const dot = Math.round(Math.min(viewport.height, viewport.width) * 0.034);
5358
+ const gap = Math.round(dot * 0.6);
5359
+ const cx = portrait ? area.left + hubR : (area.left + area.right) / 2;
5360
+ const cy = area.top + H / 2;
5361
+ const rx = portrait ? W * 0.4 : Math.min(H * 0.42, W * 0.2);
5362
+ const ry = portrait ? H * 0.42 : rx;
5363
+ const leftCount = portrait ? 0 : Math.ceil(n / 2);
5364
+ const spots = items.map((_, index) => {
5365
+ const onLeft = index < leftCount;
5366
+ const count = onLeft ? leftCount : n - leftCount;
5367
+ const at = onLeft ? index : index - leftCount;
5368
+ const spread = portrait ? 124 : count <= 1 ? 0 : count === 2 ? 70 : 110;
5369
+ const degrees = count <= 1 ? 0 : -spread / 2 + spread * at / (count - 1);
5370
+ const theta = degrees * Math.PI / 180;
5371
+ return {
5372
+ onLeft,
5373
+ x: cx + (onLeft ? -1 : 1) * rx * Math.cos(theta),
5374
+ y: cy + ry * Math.sin(theta)
5375
+ };
5376
+ });
5377
+ const labelW = (index) => spots[index].onLeft ? spots[index].x - dot / 2 - gap - area.left : area.right - (spots[index].x + dot / 2 + gap);
5378
+ const rowH = (index) => {
5379
+ const side = spots.filter((spot) => spot.onLeft === spots[index].onLeft);
5380
+ if (side.length < 2) return H * 0.8;
5381
+ return (side[side.length - 1].y - side[0].y) / (side.length - 1) * 0.9;
5382
+ };
5383
+ const scale = Math.min(
5384
+ ...items.map(
5385
+ (item, index) => fitRamp(
5386
+ [item],
5387
+ context,
5388
+ () => labelW(index),
5389
+ () => rowH(index)
5390
+ ).scale
5391
+ )
5392
+ );
5393
+ const sizes = ramp(context, scale);
5394
+ const measures = items.map((item, index) => measureItem(item, sizes, labelW(index)));
5395
+ const line = withAlpha(area.accent, 0.6);
5396
+ const stroke = Math.max(3, Math.round(viewport.height * 4e-3));
5397
+ const layers = [...area.layers];
5398
+ layers.push({
5399
+ ...disc("hub", cx, cy, hubR, area.accent),
5400
+ animation: entrance(context, "pop", 0.35)
5401
+ });
5402
+ if (center) {
5403
+ const side = hubR * 1.5;
5404
+ const floor = themedFontSize(18, context, true);
5405
+ const size = unbrokenSize(center, themedFontSize(36, context, true), side, floor);
5406
+ const fit = fitProse({
5407
+ text: center,
5408
+ baseFontSize: size,
5409
+ minFontSize: Math.min(size, floor),
5410
+ maxWidthPx: side,
5411
+ maxHeightPx: side,
5412
+ lineHeight: 1.15
4830
5413
  });
4831
5414
  layers.push({
4832
5415
  type: "text",
4833
- id: `item-${i}`,
5416
+ id: "hub-center",
5417
+ position: { x: cx, y: cy, width: side, height: side, anchor: "center" },
4834
5418
  content: {
4835
- text: itemText2,
4836
- ...itemHtml ? { html: itemHtml } : {},
4837
- paragraphs: [
4838
- {
4839
- text: itemText2,
4840
- list: { ordered, level: 0, ...ordered ? { number: i + 1 } : {} }
4841
- }
4842
- ],
4843
- flow: { id: LIST_ITEMS_FLOW },
5419
+ text: center,
4844
5420
  style: {
4845
- fontSize: itemFontSize,
4846
- fontFamily: getThemeFont(context, "body"),
4847
- color: theme.colors.text,
4848
- textAlign: "left",
4849
- lineHeight: LIST_ITEM_LINE_HEIGHT,
4850
- shadow: shouldUseShadow(context)
5421
+ fontSize: fit.fontSize,
5422
+ fontFamily: getThemeFont(context, "title"),
5423
+ fontWeight: "bold",
5424
+ color: pickContrastingText(area.accent),
5425
+ textAlign: "center",
5426
+ verticalAlign: "middle",
5427
+ lineHeight: 1.15,
5428
+ shrinkToFit: true,
5429
+ ...fit.maxLines ? { maxLines: fit.maxLines } : {}
4851
5430
  }
4852
5431
  },
4853
- position: {
4854
- x: bodyLeftX,
4855
- y: adjustY(`${itemY}%`, accentLayout),
4856
- width: bodyWidthPx,
4857
- // Rich (HTML) items render in a hidden-overflow box the renderer
4858
- // sizes from the plain text; size it here from the same line count
4859
- // that reserved the vertical space so nested sub-items are not cut off.
4860
- height: lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT + itemFontSize * 0.5
4861
- },
4862
- animation
5432
+ animation: entrance(context, "pop", 0.35)
4863
5433
  });
4864
- const itemHeightPx = lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT;
4865
- itemY += (itemHeightPx + gapPx) / context.viewport.height * 100;
4866
5434
  }
5435
+ items.forEach((item, index) => {
5436
+ const { x, y, onLeft } = spots[index];
5437
+ const length = Math.hypot(x - cx, y - cy) || 1;
5438
+ const [ux, uy] = [(x - cx) / length, (y - cy) / length];
5439
+ const from = hubR + dot * 0.35;
5440
+ const to = length - dot / 2 - dot * 0.15;
5441
+ const m = measures[index];
5442
+ const height = Number.isFinite(m.height) ? m.height : 0;
5443
+ const width = labelW(index);
5444
+ const left = onLeft ? x - dot / 2 - gap - width : x + dot / 2 + gap;
5445
+ const top = Math.min(
5446
+ Math.max(area.top, y - height / 2),
5447
+ Math.max(area.top, area.bottom - height)
5448
+ );
5449
+ layers.push(
5450
+ ...step(
5451
+ [
5452
+ [
5453
+ connector(
5454
+ `spoke-${index}`,
5455
+ `M ${cx + from * ux} ${cy + from * uy} L ${cx + to * ux} ${cy + to * uy}`,
5456
+ line,
5457
+ stroke,
5458
+ false
5459
+ ),
5460
+ "draw",
5461
+ 0
5462
+ ],
5463
+ [disc(`spoke-${index}-end`, x, y, dot / 2, area.accent), "pop", 0.3],
5464
+ ...words(
5465
+ itemText(
5466
+ `spoke-${index}`,
5467
+ item,
5468
+ m,
5469
+ sizes,
5470
+ left,
5471
+ top,
5472
+ width,
5473
+ onLeft ? "right" : "left",
5474
+ context
5475
+ ),
5476
+ 0.45
5477
+ )
5478
+ ],
5479
+ index,
5480
+ context
5481
+ )
5482
+ );
5483
+ });
5484
+ return layers;
5485
+ }
5486
+ function secondColor(accent, context) {
5487
+ const { colors, colorSchemes } = context.theme;
5488
+ const hue = (color) => isHex(color) ? hexHueDegrees(color) : NaN;
5489
+ const apart = (color) => {
5490
+ const turn = Math.abs(hue(color) - hue(accent)) % 360;
5491
+ return Math.min(turn, 360 - turn);
5492
+ };
5493
+ const candidates = [
5494
+ ...Object.values(colorSchemes ?? {}).map((scheme) => scheme.accent),
5495
+ colors.secondary,
5496
+ colors.primary,
5497
+ colors.highlight
5498
+ ];
5499
+ return candidates.find((color) => !!color && apart(color) >= 50) ?? colors.text;
5500
+ }
5501
+ function vennBlock(input, context) {
5502
+ const crowded = asListWhenCrowded(input, context);
5503
+ if (crowded) return crowded;
5504
+ const all = itemsOf(input);
5505
+ const sharedAt = vennOverlapIndex(all);
5506
+ const sets = all.map((item, index) => ({ item, index })).filter(({ index }) => index !== sharedAt);
5507
+ if (conceptDrawsAs("venn", input.items) === "cards")
5508
+ return cardsBlock({ ...input, template: "cards" }, context);
5509
+ const shared = sharedAt >= 0 ? all[sharedAt] : void 0;
5510
+ const area = frame(input, context);
5511
+ const { theme, viewport } = context;
5512
+ const W = area.right - area.left;
5513
+ const H = area.bottom - area.top;
5514
+ const portrait = area.portrait;
5515
+ const colors = [area.accent, secondColor(area.accent, context)];
5516
+ const mid = (area.left + area.right) / 2;
5517
+ const stroke = Math.max(2, Math.round(viewport.height * 3e-3));
5518
+ const gap = Math.round(viewport.width * 0.025);
5519
+ const layers = [...area.layers];
5520
+ const captionSize = themedFontSize(DETAIL_BASE * 1.1, context, false);
5521
+ const captionW = portrait ? W : W * 0.62;
5522
+ const captionLines = shared?.detail ? Math.min(3, estimateProseLineCount(shared.detail, captionSize, captionW)) : 0;
5523
+ const sharedLabelH = shared?.label && !portrait ? captionSize * 1.5 : 0;
5524
+ const band = shared && !portrait ? sharedLabelH + captionLines * captionSize * DETAIL_LINE_HEIGHT + viewport.height * 0.04 : 0;
5525
+ const circlesH = portrait ? H * 0.34 : H - band;
5526
+ const R = Math.round(Math.min(circlesH / 2, (portrait ? W : W * 0.34) / 2));
5527
+ const distance = R * 1.12;
5528
+ const cy = area.top + circlesH / 2;
5529
+ const centres = [mid - distance / 2, mid + distance / 2];
5530
+ const lensHalf = Math.sqrt(Math.max(0, R * R - (distance / 2) ** 2));
5531
+ const sideW = portrait ? W - gap * 1.5 : mid - distance / 2 - R - gap - area.left;
5532
+ const rowGap = Math.round(viewport.height * 0.025);
5533
+ const listTop = cy + R + viewport.height * 0.045;
5534
+ const listed = portrait ? [...sets.map(({ item }) => item), ...shared ? [shared] : []] : [];
5535
+ const { sizes } = fitRamp(
5536
+ portrait ? listed : sets.map(({ item }) => ({ label: "", detail: item.detail })),
5537
+ context,
5538
+ () => sideW,
5539
+ () => portrait ? (area.bottom - listTop - rowGap * (listed.length - 1)) / Math.max(1, listed.length) : circlesH * 0.9
5540
+ );
5541
+ let rowTop = listTop;
5542
+ const row = (id, item, fill) => {
5543
+ const m = measureItem(item, sizes, sideW);
5544
+ const firstLine = item.label ? sizes.label * LABEL_LINE_HEIGHT : sizes.plain * DETAIL_LINE_HEIGHT;
5545
+ const swatch = Math.round(sizes.label * 0.5);
5546
+ const parts = [
5547
+ [
5548
+ disc(`${id}-swatch`, area.left + swatch / 2, rowTop + firstLine / 2, swatch / 2, fill),
5549
+ "pop",
5550
+ 0.2
5551
+ ],
5552
+ ...words(
5553
+ itemText(id, item, m, sizes, area.left + gap * 1.5, rowTop, sideW, "left", context),
5554
+ 0.3
5555
+ )
5556
+ ];
5557
+ rowTop += (Number.isFinite(m.height) ? m.height : firstLine) + rowGap;
5558
+ return parts;
5559
+ };
5560
+ sets.forEach(({ item, index }, side) => {
5561
+ const cx = centres[side];
5562
+ const fill = colors[side];
5563
+ const parts = [
5564
+ [
5565
+ {
5566
+ type: "shape",
5567
+ id: `set-${side}`,
5568
+ position: { x: cx - R, y: cy - R, width: R * 2, height: R * 2 },
5569
+ content: {
5570
+ shape: "circle",
5571
+ fill: withAlpha(fill, 0.3),
5572
+ stroke: withAlpha(fill, 0.9),
5573
+ strokeWidth: stroke
5574
+ }
5575
+ },
5576
+ "pop",
5577
+ 0
5578
+ ]
5579
+ ];
5580
+ if (portrait) {
5581
+ parts.push(...row(`set-${side}`, item, withAlpha(fill, 0.75)));
5582
+ } else {
5583
+ const own = R * 2 - (R * 2 - distance);
5584
+ const lobeW = own * 0.74;
5585
+ const lobeX = side === 0 ? cx - R + own * 0.52 : cx + R - own * 0.52;
5586
+ if (item.label) {
5587
+ const floor = themedFontSize(20, context, true);
5588
+ const size = unbrokenSize(
5589
+ item.label,
5590
+ themedFontSize(LABEL_BASE, context, true),
5591
+ lobeW,
5592
+ floor
5593
+ );
5594
+ const fit = fitProse({
5595
+ text: item.label,
5596
+ baseFontSize: size,
5597
+ minFontSize: Math.min(size, floor),
5598
+ maxWidthPx: lobeW,
5599
+ maxHeightPx: R * 0.9,
5600
+ lineHeight: LABEL_LINE_HEIGHT
5601
+ });
5602
+ parts.push([
5603
+ {
5604
+ type: "text",
5605
+ id: `set-${side}-label`,
5606
+ position: { x: lobeX, y: cy, width: lobeW, height: R * 0.9, anchor: "center" },
5607
+ content: {
5608
+ text: item.label,
5609
+ style: {
5610
+ fontSize: fit.fontSize,
5611
+ fontFamily: getThemeFont(context, "title"),
5612
+ fontWeight: "bold",
5613
+ color: theme.colors.text,
5614
+ textAlign: "center",
5615
+ verticalAlign: "middle",
5616
+ lineHeight: LABEL_LINE_HEIGHT,
5617
+ shrinkToFit: true,
5618
+ ...fit.maxLines ? { maxLines: fit.maxLines } : {}
5619
+ }
5620
+ }
5621
+ },
5622
+ "text",
5623
+ 0.25
5624
+ ]);
5625
+ }
5626
+ if (item.detail) {
5627
+ const detail = { label: "", detail: item.detail };
5628
+ const m = measureItem(detail, sizes, sideW);
5629
+ const height = Number.isFinite(m.height) ? m.height : 0;
5630
+ const left = side === 0 ? area.left : area.right - sideW;
5631
+ parts.push(
5632
+ ...words(
5633
+ itemText(
5634
+ `set-${side}`,
5635
+ detail,
5636
+ m,
5637
+ sizes,
5638
+ left,
5639
+ Math.max(area.top, cy - height / 2),
5640
+ sideW,
5641
+ side === 0 ? "right" : "left",
5642
+ context
5643
+ ),
5644
+ 0.4
5645
+ )
5646
+ );
5647
+ }
5648
+ }
5649
+ layers.push(...step(parts, index, context));
5650
+ });
5651
+ if (shared) {
5652
+ const lens = region(
5653
+ "overlap",
5654
+ `M ${mid} ${cy - lensHalf} A ${R} ${R} 0 0 1 ${mid} ${cy + lensHalf} A ${R} ${R} 0 0 1 ${mid} ${cy - lensHalf} Z`,
5655
+ withAlpha(theme.colors.text, 0.16),
5656
+ withAlpha(theme.colors.text, 0.5),
5657
+ stroke
5658
+ );
5659
+ const parts = [[lens, "surface", 0]];
5660
+ if (portrait) {
5661
+ parts.push(...row("overlap", shared, withAlpha(theme.colors.text, 0.5)));
5662
+ } else {
5663
+ const top = cy + R + viewport.height * 0.02;
5664
+ parts.push([
5665
+ connector(
5666
+ "overlap-callout",
5667
+ `M ${mid} ${cy + lensHalf * 0.35} L ${mid} ${top}`,
5668
+ withAlpha(theme.colors.text, 0.5),
5669
+ stroke,
5670
+ false
5671
+ ),
5672
+ "draw",
5673
+ 0.2
5674
+ ]);
5675
+ if (shared.label)
5676
+ parts.push([
5677
+ textLayer(
5678
+ "overlap-label",
5679
+ shared.label,
5680
+ box(mid - captionW / 2, top + viewport.height * 0.012, captionW, sharedLabelH),
5681
+ {
5682
+ fontSize: captionSize,
5683
+ fontFamily: getThemeFont(context, "title"),
5684
+ fontWeight: "bold",
5685
+ color: theme.colors.text,
5686
+ lineHeight: LABEL_LINE_HEIGHT,
5687
+ textAlign: "center"
5688
+ }
5689
+ ),
5690
+ "text",
5691
+ 0.3
5692
+ ]);
5693
+ if (shared.detail)
5694
+ parts.push([
5695
+ textLayer(
5696
+ "overlap-detail",
5697
+ shared.detail,
5698
+ box(
5699
+ mid - captionW / 2,
5700
+ top + viewport.height * 0.012 + sharedLabelH,
5701
+ captionW,
5702
+ captionLines * captionSize * DETAIL_LINE_HEIGHT + captionSize * 0.4
5703
+ ),
5704
+ {
5705
+ fontSize: captionSize,
5706
+ fontFamily: getThemeFont(context, "body"),
5707
+ color: theme.colors.text,
5708
+ lineHeight: DETAIL_LINE_HEIGHT,
5709
+ textAlign: "center",
5710
+ maxLines: 3
5711
+ }
5712
+ ),
5713
+ "text",
5714
+ 0.4
5715
+ ]);
5716
+ }
5717
+ layers.push(...step(parts, sharedAt, context));
5718
+ }
5719
+ return layers;
5720
+ }
5721
+ var MATRIX_SCALES = [1.5, 1.32];
5722
+ function matrixBlock(input, context) {
5723
+ const crowded = asListWhenCrowded(input, context);
5724
+ if (crowded) return crowded;
5725
+ const items = itemsOf(input);
5726
+ if (conceptDrawsAs("matrix", input.items) === "cards")
5727
+ return cardsBlock({ ...input, template: "cards" }, context);
5728
+ const area = frame(input, context);
5729
+ const { theme, viewport } = context;
5730
+ const xAxis = String(input.xAxis ?? "").trim();
5731
+ const yAxis = String(input.yAxis ?? "").trim();
5732
+ const axisSize = themedFontSize(DETAIL_BASE * 1.05, context, false);
5733
+ const axisRoom = Math.round(axisSize * 1.9);
5734
+ const stroke = Math.max(3, Math.round(viewport.height * 4e-3));
5735
+ const left = area.left + (yAxis ? axisRoom * 0.7 : 0);
5736
+ const top = area.top + (yAxis ? axisRoom : 0);
5737
+ const right = area.right;
5738
+ const bottom = area.bottom - (xAxis ? axisRoom * 1.15 : 0);
5739
+ const gap = Math.round(viewport.width * 0.014);
5740
+ const cellW = (right - left - gap) / 2;
5741
+ const cellH = (bottom - top - gap) / 2;
5742
+ const pad = Math.round(Math.min(40, Math.max(22, cellW * 0.07)));
5743
+ const ruleH = Math.max(4, Math.round(viewport.height * 6e-3));
5744
+ const ruleGap = Math.round(viewport.height * 0.02);
5745
+ const chrome = pad * 2 + ruleH + ruleGap;
5746
+ const roomy = MATRIX_SCALES.find(
5747
+ (scale) => items.every(
5748
+ (item) => measureItem(item, ramp(context, scale), cellW - pad * 2).height <= (cellH - chrome) * 0.7
5749
+ )
5750
+ );
5751
+ const sizes = roomy ? ramp(context, roomy) : fitRamp(
5752
+ items,
5753
+ context,
5754
+ () => cellW - pad * 2,
5755
+ () => cellH - chrome
5756
+ ).sizes;
5757
+ const measures = items.map((item) => measureItem(item, sizes, cellW - pad * 2));
5758
+ const layers = [...area.layers];
5759
+ const axisColor = withAlpha(theme.colors.text, 0.6);
5760
+ const axisText = (id, text2, x, y, align) => textLayer(id, text2, box(x, y, (right - left) * 0.6, axisSize * 1.4), {
5761
+ fontSize: axisSize,
5762
+ fontFamily: getThemeFont(context, "title"),
5763
+ fontWeight: "bold",
5764
+ color: axisColor,
5765
+ lineHeight: 1.2,
5766
+ textAlign: align
5767
+ });
5768
+ if (yAxis) {
5769
+ const x = area.left + stroke;
5770
+ layers.push(
5771
+ {
5772
+ ...connector(
5773
+ "axis-y",
5774
+ `M ${x} ${bottom} L ${x} ${top + axisRoom * 0.1}`,
5775
+ axisColor,
5776
+ stroke
5777
+ ),
5778
+ animation: entrance(context, "draw", 0.3)
5779
+ },
5780
+ {
5781
+ ...axisText("axis-y-label", yAxis, area.left, area.top, "left"),
5782
+ animation: entrance(context, "text", 0.45)
5783
+ }
5784
+ );
5785
+ }
5786
+ if (xAxis) {
5787
+ const y = bottom + axisRoom * 0.35;
5788
+ layers.push(
5789
+ {
5790
+ ...connector("axis-x", `M ${left} ${y} L ${right - stroke} ${y}`, axisColor, stroke),
5791
+ animation: entrance(context, "draw", 0.3)
5792
+ },
5793
+ {
5794
+ ...axisText(
5795
+ "axis-x-label",
5796
+ xAxis,
5797
+ right - (right - left) * 0.6,
5798
+ y + axisRoom * 0.2,
5799
+ "right"
5800
+ ),
5801
+ animation: entrance(context, "text", 0.45)
5802
+ }
5803
+ );
5804
+ }
5805
+ items.forEach((item, index) => {
5806
+ const x = left + index % 2 * (cellW + gap);
5807
+ const y = top + Math.floor(index / 2) * (cellH + gap);
5808
+ layers.push(
5809
+ ...step(
5810
+ [
5811
+ ...panel(`cell-${index}`, x, y, cellW, cellH, pad, ruleH, area.accent, context),
5812
+ ...words(
5813
+ itemText(
5814
+ `cell-${index}`,
5815
+ item,
5816
+ measures[index],
5817
+ sizes,
5818
+ x + pad,
5819
+ y + pad + ruleH + ruleGap,
5820
+ cellW - pad * 2,
5821
+ "left",
5822
+ context
5823
+ ),
5824
+ 0.22
5825
+ )
5826
+ ],
5827
+ index,
5828
+ context
5829
+ )
5830
+ );
5831
+ });
4867
5832
  return layers;
4868
5833
  }
4869
5834
 
@@ -5176,6 +6141,8 @@ function definitionCard(input, context) {
5176
6141
  // src/doc/templates/comparisonBar.ts
5177
6142
  function comparisonBar(input, context) {
5178
6143
  const { leftLabel, rightLabel, unit, colorScheme = "blue" } = input;
6144
+ if (!leftLabel && !rightLabel && !Number.isFinite(input.leftValue) && !Number.isFinite(input.rightValue))
6145
+ return [];
5179
6146
  const { theme, viewport } = context;
5180
6147
  const colors = resolveColorScheme(context, colorScheme);
5181
6148
  const leftValue = Number.isFinite(input.leftValue) ? input.leftValue : 0;
@@ -5345,7 +6312,7 @@ var FIT_SLACK = 0.93;
5345
6312
  var QUOTE_LINE_HEIGHT = 1.35;
5346
6313
  var ATTR_LINE_HEIGHT = 1.4;
5347
6314
  function pullQuoteLockup(options, context) {
5348
- const { text, attribution, overMedia } = options;
6315
+ const { text: text2, attribution, overMedia } = options;
5349
6316
  const { theme, viewport } = context;
5350
6317
  const pct2 = (px) => px / viewport.height * 100;
5351
6318
  const quoteBaseSize = themedFontSize(52, context, true);
@@ -5364,7 +6331,7 @@ function pullQuoteLockup(options, context) {
5364
6331
  (frameHeightPx - attrHeightPx - gapReservePx) * FIT_SLACK
5365
6332
  );
5366
6333
  const fit = fitProse({
5367
- text,
6334
+ text: text2,
5368
6335
  baseFontSize: quoteBaseSize,
5369
6336
  minFontSize: quoteMinSize,
5370
6337
  maxWidthPx: columnWidthPx,
@@ -5388,7 +6355,7 @@ function pullQuoteLockup(options, context) {
5388
6355
  type: "text",
5389
6356
  id: "quote-text",
5390
6357
  content: {
5391
- text,
6358
+ text: text2,
5392
6359
  style: {
5393
6360
  fontSize: fit.fontSize,
5394
6361
  fontFamily: getThemeFont(context, "title"),
@@ -5466,7 +6433,7 @@ function pullQuoteOverlayLayer() {
5466
6433
  return createBackgroundLayer("overlay", "rgba(0, 0, 0, 0.55)");
5467
6434
  }
5468
6435
  function pullQuote(input, context) {
5469
- const { text, attribution, backgroundImage, ambientMotion } = input;
6436
+ const { text: text2, attribution, backgroundImage, ambientMotion } = input;
5470
6437
  const treatment = themedImageTreatment(context, input.imageTreatment);
5471
6438
  const layers = [];
5472
6439
  if (backgroundImage?.src) {
@@ -5496,7 +6463,7 @@ function pullQuote(input, context) {
5496
6463
  layers.push(createBackgroundLayer("bg", themedSurfaceGradient(context, 160)));
5497
6464
  }
5498
6465
  layers.push(
5499
- ...pullQuoteLockup({ text, attribution, overMedia: Boolean(backgroundImage?.src) }, context)
6466
+ ...pullQuoteLockup({ text: text2, attribution, overMedia: Boolean(backgroundImage?.src) }, context)
5500
6467
  );
5501
6468
  return layers;
5502
6469
  }
@@ -5639,7 +6606,7 @@ function videoWithCaption(input, context) {
5639
6606
 
5640
6607
  // src/doc/templates/videoPullQuote.ts
5641
6608
  function videoPullQuote(input, context) {
5642
- const { text, attribution, backgroundVideo } = input;
6609
+ const { text: text2, attribution, backgroundVideo } = input;
5643
6610
  const layers = [];
5644
6611
  if (backgroundVideo?.src) {
5645
6612
  layers.push(
@@ -5667,7 +6634,7 @@ function videoPullQuote(input, context) {
5667
6634
  layers.push(createBackgroundLayer("bg", themedSurfaceGradient(context, 160)));
5668
6635
  }
5669
6636
  layers.push(
5670
- ...pullQuoteLockup({ text, attribution, overMedia: Boolean(backgroundVideo?.src) }, context)
6637
+ ...pullQuoteLockup({ text: text2, attribution, overMedia: Boolean(backgroundVideo?.src) }, context)
5671
6638
  );
5672
6639
  return layers;
5673
6640
  }
@@ -5763,22 +6730,22 @@ function normalizeChartTable(table) {
5763
6730
  }
5764
6731
  function parseCellNumber(raw) {
5765
6732
  if (typeof raw !== "string") return null;
5766
- let text = raw.trim();
5767
- if (!text) return null;
6733
+ let text2 = raw.trim();
6734
+ if (!text2) return null;
5768
6735
  let negative = false;
5769
- const accounting = /^\((.*)\)$/.exec(text);
6736
+ const accounting = /^\((.*)\)$/.exec(text2);
5770
6737
  if (accounting) {
5771
6738
  negative = true;
5772
- text = accounting[1].trim();
6739
+ text2 = accounting[1].trim();
5773
6740
  }
5774
- text = text.replace(/^([+-]?)\s*[$€£¥]\s*/, "$1");
5775
- text = text.replace(/%$/, "").trim();
5776
- text = text.replace(/,/g, "");
5777
- if (/^[+-]?\d{1,3}(?: \d{3})+(?:\.\d+)?$/.test(text)) {
5778
- text = text.replace(/ /g, "");
6741
+ text2 = text2.replace(/^([+-]?)\s*[$€£¥]\s*/, "$1");
6742
+ text2 = text2.replace(/%$/, "").trim();
6743
+ text2 = text2.replace(/,/g, "");
6744
+ if (/^[+-]?\d{1,3}(?: \d{3})+(?:\.\d+)?$/.test(text2)) {
6745
+ text2 = text2.replace(/ /g, "");
5779
6746
  }
5780
- if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(text)) return null;
5781
- const value = Number(text);
6747
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(text2)) return null;
6748
+ const value = Number(text2);
5782
6749
  if (!Number.isFinite(value)) return null;
5783
6750
  return negative ? -value : value;
5784
6751
  }
@@ -5794,13 +6761,13 @@ function isNumericColumn(rows, colIndex) {
5794
6761
  return numeric > 0 && numeric / Math.max(1, nonEmpty) >= NUMERIC_COLUMN_THRESHOLD;
5795
6762
  }
5796
6763
  function resolveColumnRef(ref, headers) {
5797
- const text = String(ref).trim();
5798
- if (!text) return null;
5799
- const lower = text.toLowerCase();
6764
+ const text2 = String(ref).trim();
6765
+ if (!text2) return null;
6766
+ const lower = text2.toLowerCase();
5800
6767
  const byName = headers.findIndex((h) => h.trim().toLowerCase() === lower);
5801
6768
  if (byName >= 0) return byName;
5802
- if (/^\d+$/.test(text)) {
5803
- const index = Number(text);
6769
+ if (/^\d+$/.test(text2)) {
6770
+ const index = Number(text2);
5804
6771
  if (index < headers.length) return index;
5805
6772
  }
5806
6773
  return null;
@@ -5848,9 +6815,9 @@ function buildChartData(table, options = {}) {
5848
6815
  }
5849
6816
 
5850
6817
  // src/doc/templates/chart/scale.ts
5851
- function niceNum(range, round4) {
5852
- const exponent = Math.floor(Math.log10(range));
5853
- const fraction = range / 10 ** exponent;
6818
+ function niceNum(range2, round4) {
6819
+ const exponent = Math.floor(Math.log10(range2));
6820
+ const fraction = range2 / 10 ** exponent;
5854
6821
  let niceFraction;
5855
6822
  if (round4) {
5856
6823
  if (fraction < 1.5) niceFraction = 1;
@@ -5876,8 +6843,8 @@ function niceTicks(min, max, targetCount = 5) {
5876
6843
  if (min >= 0 && lo < 0) lo = 0;
5877
6844
  }
5878
6845
  const count = Math.max(2, targetCount);
5879
- const range = niceNum(hi - lo, false);
5880
- const step2 = niceNum(range / (count - 1), true);
6846
+ const range2 = niceNum(hi - lo, false);
6847
+ const step2 = niceNum(range2 / (count - 1), true);
5881
6848
  const niceMin = Math.floor(lo / step2) * step2;
5882
6849
  const niceMax = Math.ceil(hi / step2) * step2;
5883
6850
  const ticks = [];
@@ -5890,24 +6857,24 @@ function formatChartValue(value, unit) {
5890
6857
  if (!Number.isFinite(value)) return "\u2014";
5891
6858
  const sign = value < 0 ? "-" : "";
5892
6859
  const abs = Math.abs(value);
5893
- let text;
6860
+ let text2;
5894
6861
  if (abs >= 1e6) {
5895
- text = `${sign}${trimTrailingZero((abs / 1e6).toFixed(1))}M`;
6862
+ text2 = `${sign}${trimTrailingZero((abs / 1e6).toFixed(1))}M`;
5896
6863
  } else if (abs >= 1e3) {
5897
- text = `${sign}${trimTrailingZero((abs / 1e3).toFixed(abs >= 1e4 ? 0 : 1))}K`;
6864
+ text2 = `${sign}${trimTrailingZero((abs / 1e3).toFixed(abs >= 1e4 ? 0 : 1))}K`;
5898
6865
  } else {
5899
- text = `${sign}${abs.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
6866
+ text2 = `${sign}${abs.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
5900
6867
  }
5901
- return unit ? `${text} ${unit}` : text;
6868
+ return unit ? `${text2} ${unit}` : text2;
5902
6869
  }
5903
6870
  function formatChartDataValue(value, unit) {
5904
6871
  if (!Number.isFinite(value) || Math.abs(value) >= 1e4) return formatChartValue(value, unit);
5905
6872
  const sign = value < 0 ? "-" : "";
5906
- const text = `${sign}${Math.abs(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
5907
- return unit ? `${text} ${unit}` : text;
6873
+ const text2 = `${sign}${Math.abs(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
6874
+ return unit ? `${text2} ${unit}` : text2;
5908
6875
  }
5909
- function trimTrailingZero(text) {
5910
- return text.replace(/\.0$/, "");
6876
+ function trimTrailingZero(text2) {
6877
+ return text2.replace(/\.0$/, "");
5911
6878
  }
5912
6879
 
5913
6880
  // src/doc/templates/chart/geometry.ts
@@ -5944,15 +6911,15 @@ function arcPath(cx, cy, rOuter, rInner, startDeg, endDeg) {
5944
6911
  "Z"
5945
6912
  ].join(" ");
5946
6913
  }
5947
- function polylinePath(points) {
5948
- if (points.length === 0) return "";
5949
- return points.map((p, i) => `${i === 0 ? "M" : "L"} ${round(p.x)} ${round(p.y)}`).join(" ");
6914
+ function polylinePath(points2) {
6915
+ if (points2.length === 0) return "";
6916
+ return points2.map((p, i) => `${i === 0 ? "M" : "L"} ${round(p.x)} ${round(p.y)}`).join(" ");
5950
6917
  }
5951
- function areaPath(points, baselineY) {
5952
- if (points.length === 0) return "";
5953
- const open = polylinePath(points);
5954
- const first = points[0];
5955
- const last = points[points.length - 1];
6918
+ function areaPath(points2, baselineY) {
6919
+ if (points2.length === 0) return "";
6920
+ const open = polylinePath(points2);
6921
+ const first = points2[0];
6922
+ const last = points2[points2.length - 1];
5956
6923
  return `${open} L ${round(last.x)} ${round(baselineY)} L ${round(first.x)} ${round(baselineY)} Z`;
5957
6924
  }
5958
6925
 
@@ -7064,34 +8031,76 @@ var BLOCK_MEDIA_LAYOUT_POLICIES = {
7064
8031
  unconsumedMedia: "reserve-when-no-native-media",
7065
8032
  variants: TEXT_VARIANTS
7066
8033
  },
7067
- cards: {
7068
- summary: "Text-only cards by default; supplemental media is paired with the cards.",
8034
+ cards: {
8035
+ summary: "Text-only cards by default; supplemental media is paired with the cards.",
8036
+ noMedia: "template-default",
8037
+ ownership: "supplemental",
8038
+ unconsumedMedia: "reserved-slot",
8039
+ variants: DENSE_VARIANTS
8040
+ },
8041
+ process: {
8042
+ summary: "A drawn process by default; supplemental media is paired with the stages.",
8043
+ noMedia: "template-default",
8044
+ ownership: "supplemental",
8045
+ unconsumedMedia: "reserved-slot",
8046
+ variants: DENSE_VARIANTS
8047
+ },
8048
+ cycle: {
8049
+ summary: "A drawn cycle by default; supplemental media is paired with the ring.",
8050
+ noMedia: "template-default",
8051
+ ownership: "supplemental",
8052
+ unconsumedMedia: "reserved-slot",
8053
+ variants: TEXT_VARIANTS
8054
+ },
8055
+ layers: {
8056
+ summary: "Drawn concentric layers by default; supplemental media is paired with the rings.",
8057
+ noMedia: "template-default",
8058
+ ownership: "supplemental",
8059
+ unconsumedMedia: "reserved-slot",
8060
+ variants: TEXT_VARIANTS
8061
+ },
8062
+ stats: {
8063
+ summary: "Key figures by default; supplemental media is paired with the figures.",
8064
+ noMedia: "template-default",
8065
+ ownership: "supplemental",
8066
+ unconsumedMedia: "reserved-slot",
8067
+ variants: TEXT_VARIANTS
8068
+ },
8069
+ pyramid: {
8070
+ summary: "A drawn pyramid by default; supplemental media is paired with the tiers.",
7069
8071
  noMedia: "template-default",
7070
8072
  ownership: "supplemental",
7071
8073
  unconsumedMedia: "reserved-slot",
7072
- variants: DENSE_VARIANTS
8074
+ variants: TEXT_VARIANTS
7073
8075
  },
7074
- process: {
7075
- summary: "A drawn process by default; supplemental media is paired with the stages.",
8076
+ funnel: {
8077
+ summary: "A drawn funnel by default; supplemental media is paired with the stages.",
7076
8078
  noMedia: "template-default",
7077
8079
  ownership: "supplemental",
7078
8080
  unconsumedMedia: "reserved-slot",
7079
- variants: DENSE_VARIANTS
8081
+ variants: TEXT_VARIANTS
7080
8082
  },
7081
- cycle: {
7082
- summary: "A drawn cycle by default; supplemental media is paired with the ring.",
8083
+ hub: {
8084
+ summary: "A drawn hub and spokes by default; supplemental media is paired with the ideas.",
7083
8085
  noMedia: "template-default",
7084
8086
  ownership: "supplemental",
7085
8087
  unconsumedMedia: "reserved-slot",
7086
8088
  variants: TEXT_VARIANTS
7087
8089
  },
7088
- layers: {
7089
- summary: "Drawn concentric layers by default; supplemental media is paired with the rings.",
8090
+ venn: {
8091
+ summary: "Drawn overlapping circles by default; supplemental media is paired with the sets.",
7090
8092
  noMedia: "template-default",
7091
8093
  ownership: "supplemental",
7092
8094
  unconsumedMedia: "reserved-slot",
7093
8095
  variants: TEXT_VARIANTS
7094
8096
  },
8097
+ matrix: {
8098
+ summary: "A two-by-two grid by default; supplemental media is paired with the grid.",
8099
+ noMedia: "template-default",
8100
+ ownership: "supplemental",
8101
+ unconsumedMedia: "reserved-slot",
8102
+ variants: DENSE_VARIANTS
8103
+ },
7095
8104
  photoGrid: {
7096
8105
  summary: "Requires a primary image collection and owns the complete tiled media field.",
7097
8106
  noMedia: "unsupported",
@@ -7243,15 +8252,15 @@ function getBlockMediaLayoutPolicy(template) {
7243
8252
  }
7244
8253
 
7245
8254
  // src/doc/richMediaLayout.ts
7246
- function containedFeatureArea(region, aspects) {
8255
+ function containedFeatureArea(region2, aspects) {
7247
8256
  const count = aspects.length;
7248
8257
  const columns2 = Math.min(
7249
8258
  count,
7250
- Math.max(1, Math.ceil(Math.sqrt(count * region.width / region.height)))
8259
+ Math.max(1, Math.ceil(Math.sqrt(count * region2.width / region2.height)))
7251
8260
  );
7252
8261
  const rows = Math.ceil(count / columns2);
7253
- const width = region.width / columns2;
7254
- const height = region.height / rows;
8262
+ const width = region2.width / columns2;
8263
+ const height = region2.height / rows;
7255
8264
  return aspects.reduce((area, aspect) => {
7256
8265
  const ratio = aspect && Number.isFinite(aspect) && aspect > 0 ? aspect : 16 / 9;
7257
8266
  const containedWidth = Math.min(width, height * ratio);
@@ -7954,15 +8963,15 @@ var DIAGRAM_LABEL_LINE_HEIGHT = 1.25;
7954
8963
  var DIAGRAM_LABEL_HORIZONTAL_PADDING = 24;
7955
8964
  var DIAGRAM_LABEL_VERTICAL_PADDING = 16;
7956
8965
  var DIAGRAM_LABEL_MIN_FONT_SIZE = 10;
7957
- function fitDiagramLabel(text, boxWidth, boxHeight, preferredFontSize) {
8966
+ function fitDiagramLabel(text2, boxWidth, boxHeight, preferredFontSize) {
7958
8967
  const textWidth = Math.max(1, boxWidth - DIAGRAM_LABEL_HORIZONTAL_PADDING);
7959
8968
  const textHeight = Math.max(1, boxHeight - DIAGRAM_LABEL_VERTICAL_PADDING);
7960
8969
  const preferred = Math.max(DIAGRAM_LABEL_MIN_FONT_SIZE, Math.floor(preferredFontSize));
7961
8970
  let fontSize = preferred;
7962
- let lineCount = countWrappedLines(text, fontSize, textWidth);
8971
+ let lineCount = countWrappedLines(text2, fontSize, textWidth);
7963
8972
  while (fontSize > DIAGRAM_LABEL_MIN_FONT_SIZE && lineCount * fontSize * DIAGRAM_LABEL_LINE_HEIGHT > textHeight) {
7964
8973
  fontSize--;
7965
- lineCount = countWrappedLines(text, fontSize, textWidth);
8974
+ lineCount = countWrappedLines(text2, fontSize, textWidth);
7966
8975
  }
7967
8976
  return {
7968
8977
  fontSize,
@@ -7971,16 +8980,16 @@ function fitDiagramLabel(text, boxWidth, boxHeight, preferredFontSize) {
7971
8980
  textWidth
7972
8981
  };
7973
8982
  }
7974
- function countWrappedLines(text, fontSize, maxWidth) {
7975
- return text.split("\n").reduce((count, line) => count + wrappedLineCount2(line, fontSize, maxWidth), 0);
8983
+ function countWrappedLines(text2, fontSize, maxWidth) {
8984
+ return text2.split("\n").reduce((count, line) => count + wrappedLineCount2(line, fontSize, maxWidth), 0);
7976
8985
  }
7977
- function wrappedLineCount2(text, fontSize, maxWidth) {
7978
- if (!text.trim()) return 1;
8986
+ function wrappedLineCount2(text2, fontSize, maxWidth) {
8987
+ if (!text2.trim()) return 1;
7979
8988
  const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
7980
8989
  if (charsPerLine <= 0) return 1;
7981
8990
  let lines = 0;
7982
8991
  let currentLength = 0;
7983
- for (const word of text.split(/\s+/)) {
8992
+ for (const word of text2.split(/\s+/)) {
7984
8993
  const testLength = currentLength > 0 ? currentLength + 1 + word.length : word.length;
7985
8994
  if (testLength <= charsPerLine) {
7986
8995
  currentLength = testLength;
@@ -7999,6 +9008,7 @@ function wrappedLineCount2(text, fontSize, maxWidth) {
7999
9008
  }
8000
9009
 
8001
9010
  // src/doc/templates/diagramBlock.ts
9011
+ import { layoutGraph } from "@bendyline/squisq-graph";
8002
9012
  var NODE_WIDTH = 180;
8003
9013
  var NODE_HEIGHT = 64;
8004
9014
  var PADDING = 80;
@@ -8075,8 +9085,8 @@ function planCanvasFrame(template, title, context) {
8075
9085
  const { viewport } = context;
8076
9086
  const media = unconsumedMedia(context.block);
8077
9087
  const mediaRect = media.count > 0 ? resolveSupplementalMediaLayout([], template, viewport, media.count, media.aspectRatios).mediaRect : void 0;
8078
- const region = freeRegion(viewport, mediaRect);
8079
- const innerWidth = Math.max(1, region.width - PADDING * 2);
9088
+ const region2 = freeRegion(viewport, mediaRect);
9089
+ const innerWidth = Math.max(1, region2.width - PADDING * 2);
8080
9090
  let titleFit;
8081
9091
  if (title) {
8082
9092
  const baseFontSize = themedFontSize(40, context, true);
@@ -8103,7 +9113,7 @@ function planCanvasFrame(template, title, context) {
8103
9113
  maxWidthPx: widthPx,
8104
9114
  maxHeightPx: Math.min(
8105
9115
  CANVAS_DESCRIPTION_MAX_LINES * baseFontSize * CANVAS_DESCRIPTION_LINE_HEIGHT,
8106
- region.height * CANVAS_DESCRIPTION_MAX_HEIGHT_RATIO
9116
+ region2.height * CANVAS_DESCRIPTION_MAX_HEIGHT_RATIO
8107
9117
  ),
8108
9118
  lineHeight: CANVAS_DESCRIPTION_LINE_HEIGHT
8109
9119
  });
@@ -8112,13 +9122,13 @@ function planCanvasFrame(template, title, context) {
8112
9122
  const titleBlockPx = titleFit ? titleFit.heightPx + CANVAS_TITLE_GAP_PX : 0;
8113
9123
  const descriptionBlockPx = description ? CANVAS_DESCRIPTION_GAP_PX + description.fit.heightPx : 0;
8114
9124
  return {
8115
- region,
9125
+ region: region2,
8116
9126
  ...titleFit ? { title: titleFit } : {},
8117
9127
  ...description ? { description } : {},
8118
9128
  titleBlockPx,
8119
9129
  descriptionBlockPx,
8120
9130
  availW: innerWidth,
8121
- availH: Math.max(1, region.height - PADDING * 2 - titleBlockPx - descriptionBlockPx),
9131
+ availH: Math.max(1, region2.height - PADDING * 2 - titleBlockPx - descriptionBlockPx),
8122
9132
  ...mediaRect ? { mediaRect } : {}
8123
9133
  };
8124
9134
  }
@@ -8140,14 +9150,14 @@ function placeCanvasGroup(frame2, contentW, contentH) {
8140
9150
  descriptionY: offsetY + scaledH + CANVAS_DESCRIPTION_GAP_PX
8141
9151
  };
8142
9152
  }
8143
- function canvasTitleLayer(id, text, frame2, placement, context) {
9153
+ function canvasTitleLayer(id, text2, frame2, placement, context) {
8144
9154
  const fit = frame2.title;
8145
9155
  if (!fit) return void 0;
8146
9156
  return {
8147
9157
  type: "text",
8148
9158
  id,
8149
9159
  content: {
8150
- text,
9160
+ text: text2,
8151
9161
  style: {
8152
9162
  fontSize: fit.fontSize,
8153
9163
  fontFamily: getThemeFont(context, "title"),
@@ -8215,11 +9225,40 @@ function nodesFromChildren(context) {
8215
9225
  }))
8216
9226
  };
8217
9227
  }
8218
- function nodesFromTemplateData(input) {
9228
+ function placeDiagramNodes(nodes, rawEdges, direction) {
9229
+ if (nodes.length === 0 || nodes.some((node) => node.container)) return [];
9230
+ const ids = new Set(nodes.map((node) => node.id));
9231
+ const edges = (Array.isArray(rawEdges) ? rawEdges : []).filter(
9232
+ (edge) => typeof edge === "object" && edge !== null && ids.has(edge.source) && ids.has(edge.target) && edge.source !== edge.target
9233
+ ).map((edge, index) => ({ id: String(index), source: edge.source, target: edge.target }));
9234
+ const size = (node) => ({
9235
+ width: typeof node.w === "number" && node.w > 0 ? node.w : NODE_WIDTH,
9236
+ height: typeof node.h === "number" && node.h > 0 ? node.h : NODE_HEIGHT
9237
+ });
9238
+ const result = layoutGraph(
9239
+ { nodes: nodes.map((node) => ({ id: node.id, ...size(node) })), edges },
9240
+ {
9241
+ direction,
9242
+ layerSpacing: direction === "right" ? 100 : 96,
9243
+ nodeSpacing: direction === "right" ? 96 : 100,
9244
+ componentSpacing: 100,
9245
+ // The template draws its own connectors; only the positions are used.
9246
+ edgeSpacing: 1
9247
+ }
9248
+ );
9249
+ return nodes.map((node, index) => ({
9250
+ ...node,
9251
+ x: Math.round(result.nodes[index].x),
9252
+ y: Math.round(result.nodes[index].y)
9253
+ }));
9254
+ }
9255
+ function nodesFromTemplateData(input, direction) {
8219
9256
  const rawNodes = Array.isArray(input.nodes) ? input.nodes : [];
8220
- const valid = rawNodes.filter(
8221
- (n) => typeof n === "object" && n !== null && typeof n.id === "string" && n.id.length > 0 && typeof n.label === "string" && typeof n.x === "number" && Number.isFinite(n.x) && typeof n.y === "number" && Number.isFinite(n.y)
9257
+ const named = rawNodes.filter(
9258
+ (n) => typeof n === "object" && n !== null && typeof n.id === "string" && n.id.length > 0 && typeof n.label === "string"
8222
9259
  );
9260
+ const positioned = (n) => typeof n.x === "number" && Number.isFinite(n.x) && typeof n.y === "number" && Number.isFinite(n.y);
9261
+ const valid = named.some(positioned) ? named.filter(positioned) : placeDiagramNodes(named, input.edges, direction);
8223
9262
  const ids = new Set(valid.map((n) => n.id));
8224
9263
  const containerIds = new Set(
8225
9264
  valid.map((n) => n.container).filter((c) => typeof c === "string" && ids.has(c))
@@ -8264,7 +9303,7 @@ function diagramBlock(input, context) {
8264
9303
  const { theme, viewport, children = [] } = context;
8265
9304
  const colors = input.colorScheme ? resolveColorScheme(context, input.colorScheme) : void 0;
8266
9305
  const background = input.background === "surface" ? [createBackgroundLayer("diagram-bg", themedSurfaceGradient(context, 170))] : [];
8267
- const resolved = children.length > 0 ? nodesFromChildren(context) : input.nodes && input.nodes.length > 0 ? nodesFromTemplateData(input) : { nodes: [], edges: [] };
9306
+ const resolved = children.length > 0 ? nodesFromChildren(context) : input.nodes && input.nodes.length > 0 ? nodesFromTemplateData(input, viewport.height > viewport.width ? "down" : "right") : { nodes: [], edges: [] };
8268
9307
  if (resolved.nodes.length === 0) {
8269
9308
  return [
8270
9309
  ...background,
@@ -8584,11 +9623,11 @@ function coerceTracks(raw) {
8584
9623
  const tracks = [];
8585
9624
  raw.forEach((candidate, trackIndex) => {
8586
9625
  if (typeof candidate !== "object" || candidate === null) return;
8587
- const record = candidate;
8588
- const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : `track-${trackIndex + 1}`;
9626
+ const record2 = candidate;
9627
+ const id = typeof record2.id === "string" && record2.id.trim() ? record2.id.trim() : `track-${trackIndex + 1}`;
8589
9628
  const events = [];
8590
- if (Array.isArray(record.events)) {
8591
- record.events.forEach((eventCandidate, eventIndex) => {
9629
+ if (Array.isArray(record2.events)) {
9630
+ record2.events.forEach((eventCandidate, eventIndex) => {
8592
9631
  if (typeof eventCandidate !== "object" || eventCandidate === null) return;
8593
9632
  const event = eventCandidate;
8594
9633
  if (typeof event.label !== "string") return;
@@ -8612,8 +9651,8 @@ function coerceTracks(raw) {
8612
9651
  }
8613
9652
  tracks.push({
8614
9653
  id,
8615
- ...typeof record.label === "string" && record.label.trim() ? { label: record.label.trim() } : {},
8616
- ...typeof record.endLabel === "string" && record.endLabel.trim() ? { endLabel: record.endLabel.trim() } : {},
9654
+ ...typeof record2.label === "string" && record2.label.trim() ? { label: record2.label.trim() } : {},
9655
+ ...typeof record2.endLabel === "string" && record2.endLabel.trim() ? { endLabel: record2.endLabel.trim() } : {},
8617
9656
  events
8618
9657
  });
8619
9658
  });
@@ -9389,8 +10428,9 @@ function drawingBlock(input, context) {
9389
10428
  const defaultStroke = input.stroke ?? colors.text ?? theme.colors.text;
9390
10429
  const defaultFill = input.fill ?? "none";
9391
10430
  const layout = computeDrawingLayout(children);
10431
+ const background = createBackgroundLayer("bg", theme.colors.background);
9392
10432
  if (layout.shapes.length === 0 && layout.connectors.length === 0) {
9393
- return [emptyHint(input, context)];
10433
+ return [background, emptyHint(input, context)];
9394
10434
  }
9395
10435
  const minX = Math.min(...layout.shapes.map((s) => s.x));
9396
10436
  const maxX = Math.max(...layout.shapes.map((s) => s.x + s.width));
@@ -9404,7 +10444,7 @@ function drawingBlock(input, context) {
9404
10444
  const tx = (x) => offsetX + (x - minX) * scale;
9405
10445
  const ty = (y) => offsetY + (y - minY) * scale;
9406
10446
  const fontAdj = Math.min(Math.max(scale, 1), 1.5);
9407
- const layers = [];
10447
+ const layers = [background];
9408
10448
  if (input.title) {
9409
10449
  const titleLayer = canvasTitleLayer("drawing-title", input.title, frame2, placement, context);
9410
10450
  if (titleLayer) layers.push(titleLayer);
@@ -9550,12 +10590,12 @@ function shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj
9550
10590
  }
9551
10591
  return out;
9552
10592
  }
9553
- function textLayer3(id, text, cx, cy, width, context, color, bold, fontAdj) {
10593
+ function textLayer3(id, text2, cx, cy, width, context, color, bold, fontAdj) {
9554
10594
  return {
9555
10595
  type: "text",
9556
10596
  id,
9557
10597
  content: {
9558
- text,
10598
+ text: text2,
9559
10599
  style: {
9560
10600
  fontSize: themedFontSize(Math.round((bold ? 22 : 18) * fontAdj), context, false),
9561
10601
  fontFamily: getThemeFont(context, "body"),
@@ -9660,7 +10700,7 @@ function computeLayoutLayers(children, _viewport, defaults = {}) {
9660
10700
  continue;
9661
10701
  }
9662
10702
  if (kind === "text") {
9663
- const text = bodyText2(child) || child.title || "";
10703
+ const text2 = bodyText2(child) || child.title || "";
9664
10704
  const layer = {
9665
10705
  id: child.id,
9666
10706
  type: "text",
@@ -9670,7 +10710,7 @@ function computeLayoutLayers(children, _viewport, defaults = {}) {
9670
10710
  width: numParam2(params, "width") ?? DEFAULT_TEXT.w,
9671
10711
  height: numParam2(params, "height") ?? DEFAULT_TEXT.h
9672
10712
  },
9673
- content: { text, style: textStyleFromParams(params, ink) }
10713
+ content: { text: text2, style: textStyleFromParams(params, ink) }
9674
10714
  };
9675
10715
  layers.push(layer);
9676
10716
  continue;
@@ -9771,7 +10811,8 @@ function round3(n) {
9771
10811
  function layoutBlock(input, context) {
9772
10812
  const { theme, viewport, children = [] } = context;
9773
10813
  const { layers } = computeLayoutLayers(children, viewport, { ink: theme.colors.text });
9774
- return layers.length > 0 ? layers : [emptyHint2(input, context)];
10814
+ const background = createBackgroundLayer("bg", theme.colors.background);
10815
+ return [background, ...layers.length > 0 ? layers : [emptyHint2(input, context)]];
9775
10816
  }
9776
10817
  function emptyHint2(input, context) {
9777
10818
  return {
@@ -10006,12 +11047,12 @@ function handoffLayers(input, context, accent, textColor, bubbleFill) {
10006
11047
  }
10007
11048
  return layers;
10008
11049
  }
10009
- function nameLayer(id, text, x, fontSize, fontFamily, color) {
11050
+ function nameLayer(id, text2, x, fontSize, fontFamily, color) {
10010
11051
  return {
10011
11052
  type: "text",
10012
11053
  id,
10013
11054
  content: {
10014
- text,
11055
+ text: text2,
10015
11056
  style: { fontSize, fontFamily, fontWeight: "bold", color, textAlign: "center" }
10016
11057
  },
10017
11058
  position: { x, y: "42%", anchor: "center" }
@@ -10105,6 +11146,12 @@ var templateRegistry = {
10105
11146
  process: processBlock,
10106
11147
  cycle: cycleBlock,
10107
11148
  layers: layersBlock,
11149
+ stats: statsBlock,
11150
+ pyramid: pyramidBlock,
11151
+ funnel: funnelBlock,
11152
+ hub: hubBlock,
11153
+ venn: vennBlock,
11154
+ matrix: matrixBlock,
10108
11155
  photoGrid,
10109
11156
  definitionCard,
10110
11157
  comparisonBar,
@@ -10510,6 +11557,27 @@ var TEMPLATE_INPUT_DESCRIPTORS = {
10510
11557
  { key: "tracks", description: "Timeline tracks and events (supply via template data)" },
10511
11558
  { key: "links", description: "Branch links between event ids (supply via template data)" }
10512
11559
  ],
11560
+ cards: CONCEPT_DESCRIPTORS([
11561
+ {
11562
+ key: "versus",
11563
+ description: 'Set exactly two ideas against each other, with a "vs" mark between them',
11564
+ coerce: "boolean"
11565
+ }
11566
+ ]),
11567
+ process: CONCEPT_DESCRIPTORS(),
11568
+ cycle: CONCEPT_DESCRIPTORS(),
11569
+ layers: CONCEPT_DESCRIPTORS(),
11570
+ stats: CONCEPT_DESCRIPTORS(),
11571
+ pyramid: CONCEPT_DESCRIPTORS(),
11572
+ funnel: CONCEPT_DESCRIPTORS(),
11573
+ hub: CONCEPT_DESCRIPTORS([
11574
+ { key: "center", description: "The one to three words written inside the hub" }
11575
+ ]),
11576
+ venn: CONCEPT_DESCRIPTORS(),
11577
+ matrix: CONCEPT_DESCRIPTORS([
11578
+ { key: "xAxis", description: "What grows from left to right, named along the foot" },
11579
+ { key: "yAxis", description: "What grows from bottom to top, named up the side" }
11580
+ ]),
10513
11581
  videoWithCaption: [
10514
11582
  { key: "videoSrc", description: "Path to the video file", required: true },
10515
11583
  { key: "posterSrc", description: "Poster frame path" },
@@ -10522,6 +11590,17 @@ var TEMPLATE_INPUT_DESCRIPTORS = {
10522
11590
  { key: "videoLicense", description: "License identifier" }
10523
11591
  ]
10524
11592
  };
11593
+ function CONCEPT_DESCRIPTORS(extra = []) {
11594
+ return [
11595
+ { key: "title", description: "Headline above the ideas (defaults to the heading)" },
11596
+ {
11597
+ key: "colorScheme",
11598
+ description: "Accent color (defaults to the theme highlight)",
11599
+ valueHint: COLOR_SCHEME_HINT
11600
+ },
11601
+ ...extra
11602
+ ];
11603
+ }
10525
11604
  function FEATURE_DESCRIPTORS() {
10526
11605
  return [
10527
11606
  { key: "imageSrc", description: "Path to the feature image", required: true },
@@ -10726,9 +11805,9 @@ function fallbackBlockLayers(block, context, notice) {
10726
11805
  }
10727
11806
  function extractBodyText(block) {
10728
11807
  if (!block.contents || block.contents.length === 0) return "";
10729
- const text = block.contents.filter((node) => !isPictureFence(node)).map((node) => extractPlainText(node)).join("\n").trim();
10730
- if (text.length <= MAX_BODY_CHARS) return text;
10731
- return `${text.slice(0, MAX_BODY_CHARS).trimEnd()}\u2026`;
11808
+ const text2 = block.contents.filter((node) => !isPictureFence(node)).map((node) => extractPlainText(node)).join("\n").trim();
11809
+ if (text2.length <= MAX_BODY_CHARS) return text2;
11810
+ return `${text2.slice(0, MAX_BODY_CHARS).trimEnd()}\u2026`;
10732
11811
  }
10733
11812
 
10734
11813
  // src/doc/structuredData.ts
@@ -11056,6 +12135,21 @@ function materializeBlockLayersWithRuntime(block, options, runtime) {
11056
12135
  const context = runtime.templateContext ? cloneData(runtime.templateContext) : createTemplateContext(theme, blockIndex, totalBlocks, viewport, { motion: options.motion });
11057
12136
  context.block = cloneData(block);
11058
12137
  const execution = executeTemplateMaterialization(block, context, runtime.registry);
12138
+ if (execution.status === "ok" && drawsNothing(execution.layers) && hasContentToShow(block)) {
12139
+ const asContent = materializeBlockLayersWithRuntime(
12140
+ { ...block, template: "content" },
12141
+ options,
12142
+ runtime
12143
+ );
12144
+ return {
12145
+ ...asContent,
12146
+ diagnostic: {
12147
+ code: "template-drew-nothing",
12148
+ template: block.template,
12149
+ message: `Template "${block.template}" has nothing to draw here; the block is shown as content.`
12150
+ }
12151
+ };
12152
+ }
11059
12153
  if (execution.status === "ok") {
11060
12154
  const styledLayers = runtime.applyRenderStyle !== false ? applyRenderStyleToLayers(execution.layers, block, theme) : execution.layers;
11061
12155
  const layers = appendRichContentLayers(
@@ -11116,6 +12210,16 @@ function materializeBlockLayersWithRuntime(block, options, runtime) {
11116
12210
  diagnostic
11117
12211
  };
11118
12212
  }
12213
+ function drawsNothing(layers) {
12214
+ return layers.every(
12215
+ (layer) => (layer.type === "image" || layer.type === "video") && !layer.content.src
12216
+ );
12217
+ }
12218
+ function hasContentToShow(block) {
12219
+ if (resolveTemplateName(block.template) === "content") return false;
12220
+ const { title, contents } = block;
12221
+ return !!title?.trim() || (contents?.length ?? 0) > 0;
12222
+ }
11119
12223
  function mermaidSources(block) {
11120
12224
  return (block.contents ?? []).filter(
11121
12225
  (node) => node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid"
@@ -11249,15 +12353,15 @@ function collectRichMediaItems(layers, block) {
11249
12353
  ...embeddedSpatialMedia(block)
11250
12354
  ];
11251
12355
  }
11252
- function gridCells(region, count, viewport, inset) {
12356
+ function gridCells(region2, count, viewport, inset) {
11253
12357
  const unit = Math.min(viewport.width, viewport.height);
11254
12358
  const padding = inset ? unit * 0.025 : 0;
11255
12359
  const gap = count > 1 ? unit * 0.018 : 0;
11256
12360
  const inner = {
11257
- x: region.x + padding,
11258
- y: region.y + padding,
11259
- width: Math.max(0, region.width - padding * 2),
11260
- height: Math.max(0, region.height - padding * 2)
12361
+ x: region2.x + padding,
12362
+ y: region2.y + padding,
12363
+ width: Math.max(0, region2.width - padding * 2),
12364
+ height: Math.max(0, region2.height - padding * 2)
11261
12365
  };
11262
12366
  const columns2 = Math.min(
11263
12367
  count,
@@ -11407,7 +12511,42 @@ function applyEmbeddedVideoTiming(layers, block) {
11407
12511
  };
11408
12512
  });
11409
12513
  }
12514
+ function applyEmbeddedVideoEdits(layers, block) {
12515
+ const editedBySrc = new Map(
12516
+ extractEmbeddedVideos(block.contents).filter((video) => video.edits).map((video) => [video.src, video])
12517
+ );
12518
+ if (editedBySrc.size === 0) return layers;
12519
+ return layers.map((layer) => {
12520
+ if (layer.type !== "video") return layer;
12521
+ const video = editedBySrc.get(layer.content.src);
12522
+ if (!video?.edits) return layer;
12523
+ const { fx, gain, fadeIn, fadeOut, crop } = video.edits;
12524
+ const edits = {
12525
+ ...fx ? { fx } : {},
12526
+ ...gain ? { gain } : {},
12527
+ ...fadeIn ? { fadeIn } : {},
12528
+ ...fadeOut ? { fadeOut } : {},
12529
+ ...crop ? { crop } : {}
12530
+ };
12531
+ const hasEdits = Object.keys(edits).length > 0;
12532
+ const withEdits = (content) => hasEdits ? { ...layer, content: { ...content, edits } } : content === layer.content ? layer : { ...layer, content };
12533
+ if (!video.cuts?.length) return withEdits(layer.content);
12534
+ const { clipStart } = layer.content;
12535
+ const clipEnd = video.clipEnd != null ? layer.content.clipEnd : createMediaTimeMap({ clipStart, cuts: video.cuts }).playedToSource(
12536
+ Math.max(0, layer.content.clipEnd - clipStart)
12537
+ );
12538
+ const cuts = normalizeMediaCuts(video.cuts, clipStart, clipEnd);
12539
+ if (cuts.length === 0) return withEdits(layer.content);
12540
+ return withEdits({ ...layer.content, clipEnd, cuts });
12541
+ });
12542
+ }
11410
12543
  function appendRichContentLayers(layers, block, theme, viewport, mediaAspectRatios) {
12544
+ return applyEmbeddedVideoEdits(
12545
+ placeRichContentLayers(layers, block, theme, viewport, mediaAspectRatios),
12546
+ block
12547
+ );
12548
+ }
12549
+ function placeRichContentLayers(layers, block, theme, viewport, mediaAspectRatios) {
11411
12550
  const timedLayers = applyEmbeddedVideoTiming(layers, block);
11412
12551
  const items = collectRichMediaItems(timedLayers, block);
11413
12552
  if (items.length === 0) return timedLayers;
@@ -11631,6 +12770,30 @@ var TEMPLATE_METADATA = {
11631
12770
  label: "Layers",
11632
12771
  description: "Draws nested parts as concentric rings, innermost first, each one called out."
11633
12772
  },
12773
+ stats: {
12774
+ label: "Key Numbers",
12775
+ description: "Sets two to six key figures side by side, each counting up over what it measures."
12776
+ },
12777
+ pyramid: {
12778
+ label: "Pyramid",
12779
+ description: "Stacks a hierarchy as a pyramid, the apex first and the base last."
12780
+ },
12781
+ funnel: {
12782
+ label: "Funnel",
12783
+ description: "Draws stages that narrow as a funnel, the widest first and the outcome last."
12784
+ },
12785
+ hub: {
12786
+ label: "Hub",
12787
+ description: "Arranges ideas around the one thing they share, each joined to it by a spoke."
12788
+ },
12789
+ venn: {
12790
+ label: "Venn",
12791
+ description: "Draws two sets as overlapping circles, with what they share in the overlap."
12792
+ },
12793
+ matrix: {
12794
+ label: "Matrix",
12795
+ description: "Places four ideas on two axes as a two-by-two grid."
12796
+ },
11634
12797
  photoGrid: {
11635
12798
  label: "Photo Grid",
11636
12799
  description: "Arranges up to four photos in a mosaic grid."
@@ -11844,6 +13007,42 @@ var TEMPLATE_AUTHORING_METADATA = {
11844
13007
  safeForContentFirst: false,
11845
13008
  placement: "heading"
11846
13009
  },
13010
+ stats: {
13011
+ role: "highlight",
13012
+ bodyPolicy: "derived",
13013
+ safeForContentFirst: false,
13014
+ placement: "heading"
13015
+ },
13016
+ pyramid: {
13017
+ role: "content",
13018
+ bodyPolicy: "derived",
13019
+ safeForContentFirst: false,
13020
+ placement: "heading"
13021
+ },
13022
+ funnel: {
13023
+ role: "content",
13024
+ bodyPolicy: "derived",
13025
+ safeForContentFirst: false,
13026
+ placement: "heading"
13027
+ },
13028
+ hub: {
13029
+ role: "content",
13030
+ bodyPolicy: "derived",
13031
+ safeForContentFirst: false,
13032
+ placement: "heading"
13033
+ },
13034
+ venn: {
13035
+ role: "comparison",
13036
+ bodyPolicy: "derived",
13037
+ safeForContentFirst: false,
13038
+ placement: "heading"
13039
+ },
13040
+ matrix: {
13041
+ role: "comparison",
13042
+ bodyPolicy: "derived",
13043
+ safeForContentFirst: false,
13044
+ placement: "heading"
13045
+ },
11847
13046
  photoGrid: {
11848
13047
  role: "media",
11849
13048
  bodyPolicy: "structured",
@@ -11960,12 +13159,217 @@ var TEMPLATE_AUTHORING_METADATA = {
11960
13159
  }
11961
13160
  };
11962
13161
 
13162
+ // src/transform/presentationPlan.ts
13163
+ var PRESENTATION_KEY = "squisq-presentation";
13164
+ var PRESENTATION_LAYOUTS = [
13165
+ "title",
13166
+ "divider",
13167
+ "statement",
13168
+ "emphasis",
13169
+ "quote",
13170
+ "stat",
13171
+ "definition",
13172
+ "list",
13173
+ "comparison",
13174
+ "steps",
13175
+ "image",
13176
+ "cards",
13177
+ "process",
13178
+ "cycle",
13179
+ "layers",
13180
+ "stats",
13181
+ "pyramid",
13182
+ "funnel",
13183
+ "hub",
13184
+ "venn",
13185
+ "matrix"
13186
+ ];
13187
+ var AUTHORED_PRESENTATION_LAYOUT = "authored";
13188
+ var EMPHASIS_CHARACTERS = 60;
13189
+ var spaced = (text2) => text2.replace(/\s+/gu, " ").trim();
13190
+ function presentationStat(headline, points2) {
13191
+ for (const [index, line] of [headline, ...points2].entries()) {
13192
+ const match = matchNumberHighlight(line);
13193
+ if (match) return { match, line, index: index - 1 };
13194
+ }
13195
+ return null;
13196
+ }
13197
+ function presentationLayoutFits(layout, headline, points2, passage) {
13198
+ return presentationLayoutProblem(layout, headline, points2, passage) === void 0;
13199
+ }
13200
+ function presentationLayoutProblem(layout, headline, points2, passage) {
13201
+ const concept = conceptTemplate(layout);
13202
+ if (concept) return conceptItemsProblem(concept, points2, { max: PRESENTATION_LIMITS.points });
13203
+ switch (layout) {
13204
+ case "list":
13205
+ case "steps":
13206
+ return points2.length >= 2 ? void 0 : `${layout} takes at least 2 points and this has ${points2.length}`;
13207
+ case "comparison":
13208
+ return points2.length === 2 ? void 0 : `comparison takes exactly 2 points and this has ${points2.length}`;
13209
+ case "definition":
13210
+ return points2.length >= 1 ? void 0 : "definition takes a point that defines its headline";
13211
+ case "stat":
13212
+ return presentationStat(headline, points2) !== null ? void 0 : "stat shows a prominent figure, such as 40% or $3 million, and neither the headline nor a point states one";
13213
+ case "emphasis":
13214
+ return headline.length <= EMPHASIS_CHARACTERS ? void 0 : `emphasis takes a headline of at most ${EMPHASIS_CHARACTERS} characters`;
13215
+ case "quote":
13216
+ return spaced(passage).includes(spaced(headline)) ? void 0 : "quote takes a headline copied word for word from the passage";
13217
+ default:
13218
+ return void 0;
13219
+ }
13220
+ }
13221
+ function presentationLayoutNeeds(layout, wording = {}) {
13222
+ const points2 = wording.points ?? "points";
13223
+ const concept = conceptTemplate(layout);
13224
+ if (concept) {
13225
+ const count = `${conceptItemCount(concept, { max: PRESENTATION_LIMITS.points })} ${points2}`;
13226
+ if (concept === "stats") return `${count}, each stating a figure`;
13227
+ if (concept === "venn") return `${count}, one of them labelled "Both:"`;
13228
+ return count;
13229
+ }
13230
+ switch (layout) {
13231
+ case "list":
13232
+ case "steps":
13233
+ return `at least 2 ${points2}`;
13234
+ case "comparison":
13235
+ return `exactly 2 ${points2}`;
13236
+ case "stat":
13237
+ return "a prominent figure, such as 40% or $3 million";
13238
+ case "emphasis":
13239
+ return `a headline of ${EMPHASIS_CHARACTERS} characters or fewer`;
13240
+ case "quote":
13241
+ return "a sentence of 110 characters or fewer to quote";
13242
+ case "image":
13243
+ return "a picture";
13244
+ default:
13245
+ return void 0;
13246
+ }
13247
+ }
13248
+ var BEAT_EXTRAS = ["center", "xAxis", "yAxis"];
13249
+ var LAYOUT_EXTRAS = {
13250
+ hub: ["center"],
13251
+ matrix: ["xAxis", "yAxis"]
13252
+ };
13253
+ function presentationBeatExtras(layout, source) {
13254
+ const extras = {};
13255
+ if (!record(source)) return extras;
13256
+ for (const key of LAYOUT_EXTRAS[layout] ?? []) {
13257
+ const value = source[key];
13258
+ const clean = typeof value === "string" ? value.replace(/\s+/gu, " ").trim() : "";
13259
+ if (clean && text(clean, PRESENTATION_LIMITS.extraCharacters)) extras[key] = clean;
13260
+ }
13261
+ return extras;
13262
+ }
13263
+ var PRESENTATION_LIMITS = {
13264
+ sourceCharacters: 4e4,
13265
+ beats: 80,
13266
+ serializedCharacters: 16e4,
13267
+ mediaPerBeat: 4,
13268
+ /** The most points a summary slide holds. */
13269
+ points: 4,
13270
+ /** The longest a hub's centre or a matrix's axis name may be. */
13271
+ extraCharacters: 40
13272
+ };
13273
+ function record(value) {
13274
+ return !!value && typeof value === "object" && !Array.isArray(value);
13275
+ }
13276
+ function keys(value, required, optional = []) {
13277
+ return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
13278
+ }
13279
+ function text(value, max) {
13280
+ return typeof value === "string" && value.trim().length > 0 && value.length <= max && !Array.from(value).some((character) => {
13281
+ const code = character.charCodeAt(0);
13282
+ return code < 32 && code !== 9 && code !== 10 && code !== 13;
13283
+ });
13284
+ }
13285
+ function parsePresentationPlan(input) {
13286
+ let value = input;
13287
+ if (typeof value === "string") {
13288
+ if (value.length > PRESENTATION_LIMITS.serializedCharacters) return null;
13289
+ try {
13290
+ value = JSON.parse(value);
13291
+ } catch {
13292
+ return null;
13293
+ }
13294
+ }
13295
+ if (!record(value) || !keys(value, ["version", "sourceText", "origin", "beats"]) || value.version !== 1 || !text(value.sourceText, PRESENTATION_LIMITS.sourceCharacters) || typeof value.origin !== "string" || !["automatic", "ai"].includes(value.origin) || !Array.isArray(value.beats) || !value.beats.length || value.beats.length > PRESENTATION_LIMITS.beats)
13296
+ return null;
13297
+ const beats = [];
13298
+ const ids = /* @__PURE__ */ new Set();
13299
+ let end = 0;
13300
+ for (const beat of value.beats) {
13301
+ if (!record(beat) || !keys(
13302
+ beat,
13303
+ ["id", "sourceStart", "sourceEnd", "layout", "headline", "points"],
13304
+ ["imageSrc", "media", "hidden", ...BEAT_EXTRAS]
13305
+ ) || "hidden" in beat && beat.hidden !== true || !text(beat.id, 80) || ids.has(beat.id) || !Number.isInteger(beat.sourceStart) || !Number.isInteger(beat.sourceEnd) || typeof beat.sourceStart !== "number" || typeof beat.sourceEnd !== "number" || beat.sourceStart < end || beat.sourceEnd <= beat.sourceStart || beat.sourceEnd > value.sourceText.length || value.sourceText.slice(end, beat.sourceStart).trim() || !text(beat.headline, 110) || !Array.isArray(beat.points) || beat.points.length > PRESENTATION_LIMITS.points || !beat.points.every((point) => text(point, 180)))
13306
+ return null;
13307
+ const extras = presentationBeatExtras(String(beat.layout), beat);
13308
+ if (BEAT_EXTRAS.some((key) => key in beat && beat[key] !== extras[key])) return null;
13309
+ if (beat.layout === AUTHORED_PRESENTATION_LAYOUT) {
13310
+ if (beat.points.length || "media" in beat) return null;
13311
+ } else if (!PRESENTATION_LAYOUTS.includes(beat.layout) || !presentationLayoutFits(
13312
+ beat.layout,
13313
+ beat.headline,
13314
+ beat.points,
13315
+ value.sourceText.slice(beat.sourceStart, beat.sourceEnd)
13316
+ ))
13317
+ return null;
13318
+ if (beat.layout === "image" ? !text(beat.imageSrc, 2048) : "imageSrc" in beat) return null;
13319
+ if ("media" in beat && (!Array.isArray(beat.media) || !beat.media.length || beat.media.length > PRESENTATION_LIMITS.mediaPerBeat || new Set(beat.media).size !== beat.media.length || !beat.media.every((ref) => text(ref, 1100))))
13320
+ return null;
13321
+ ids.add(beat.id);
13322
+ end = beat.sourceEnd;
13323
+ beats.push({
13324
+ id: beat.id,
13325
+ sourceStart: beat.sourceStart,
13326
+ sourceEnd: beat.sourceEnd,
13327
+ layout: beat.layout,
13328
+ headline: beat.headline,
13329
+ points: [...beat.points],
13330
+ ...beat.layout === "image" ? { imageSrc: beat.imageSrc } : {},
13331
+ ..."media" in beat ? { media: [...beat.media] } : {},
13332
+ ...beat.hidden === true ? { hidden: true } : {},
13333
+ ...extras
13334
+ });
13335
+ }
13336
+ if (value.sourceText.slice(end).trim()) return null;
13337
+ return {
13338
+ version: 1,
13339
+ sourceText: value.sourceText,
13340
+ origin: value.origin,
13341
+ beats
13342
+ };
13343
+ }
13344
+ function serializePresentationPlan(plan) {
13345
+ const parsed = parsePresentationPlan(plan);
13346
+ if (!parsed) throw new Error("The presentation plan is invalid.");
13347
+ const result = JSON.stringify(parsed);
13348
+ if (result.length > PRESENTATION_LIMITS.serializedCharacters)
13349
+ throw new Error("The presentation plan is too large.");
13350
+ return result;
13351
+ }
13352
+
11963
13353
  // src/doc/templates/authoringGuide.ts
11964
13354
  var CHART = {
11965
13355
  fields: "title, headers [label, value], rows [[label, value], \u2026] 2\u20136 rows, showValues: true",
11966
13356
  use: "only values the {source} states"
11967
13357
  };
11968
13358
  var CONCEPT_BUILD = 'build: {mode: "items"} to reveal them as they are spoken';
13359
+ function conceptFields(template, items) {
13360
+ const spec = CONCEPT_SPECS[template];
13361
+ const inputs = spec.inputs ?? [];
13362
+ const form = spec.itemForm === "figure-caption" ? '"Figure: what it measures"' : '"Label: detail"';
13363
+ return [
13364
+ "title",
13365
+ // What names the picture comes before its items; a switch comes after.
13366
+ ...inputs.filter((input) => input.key !== "versus").map((input) => input.guide),
13367
+ items ?? `items: [${form}, \u2026] ${conceptItemCount(template)}${spec.order ? ` ${spec.order}` : ""}`,
13368
+ ...inputs.filter((input) => input.key === "versus").map((input) => input.guide),
13369
+ CONCEPT_BUILD
13370
+ ].join(", ");
13371
+ }
13372
+ var points = (template) => conceptItemCount(template, { max: PRESENTATION_LIMITS.points });
11969
13373
  var TEMPLATE_GUIDES = {
11970
13374
  imageWithCaption: {
11971
13375
  fields: "imageSrc, imageAlt, caption \u2264 90 chars, ambientMotion: zoomIn|zoomOut|panLeft|panRight",
@@ -12002,7 +13406,7 @@ var TEMPLATE_GUIDES = {
12002
13406
  use: "3\u20137 dated events the {source} states"
12003
13407
  },
12004
13408
  diagram: {
12005
- fields: "title, nodeShape: 'pill', nodes: [{ id, label \u2264 3 words, x, y }] 3\u20136 nodes with x in {0, 280, 560} and y in {0, 160, 320}, edges: [{ source, target, label \u2264 3 words }]",
13409
+ fields: "title, nodeShape: 'pill', nodes: [{ id, label \u2264 3 words }] 3\u20136 nodes (no x or y: the diagram places them from the edges), edges: [{ source, target, label \u2264 3 words }]",
12006
13410
  use: "relationships the {source} describes"
12007
13411
  },
12008
13412
  list: {
@@ -12021,24 +13425,54 @@ var TEMPLATE_GUIDES = {
12021
13425
  use: "opens a section on a photograph"
12022
13426
  },
12023
13427
  cards: {
12024
- fields: `title, items: ["Label: detail", \u2026] 2\u20134, versus: true for exactly two set against each other, ${CONCEPT_BUILD}`,
13428
+ fields: conceptFields("cards"),
12025
13429
  use: "parallel ideas side by side, such as kinds, causes or features"
12026
13430
  },
12027
13431
  process: {
12028
- fields: `title, items: ["Label: detail", \u2026] 2\u20134 in order, ${CONCEPT_BUILD}`,
13432
+ fields: conceptFields("process"),
12029
13433
  use: "ordered stages or dated events, drawn as numbered steps joined by arrows"
12030
13434
  },
12031
13435
  cycle: {
12032
- fields: `title, items: ["Label: detail", \u2026] 3\u20135, ${CONCEPT_BUILD}`,
13436
+ fields: conceptFields("cycle"),
12033
13437
  use: "stages that repeat in a loop, drawn on a ring"
12034
13438
  },
12035
13439
  layers: {
12036
- fields: `title, items: ["Label: detail", \u2026] 3\u20135 innermost first, ${CONCEPT_BUILD}`,
13440
+ fields: conceptFields("layers"),
12037
13441
  use: "parts nested inside one another, drawn as concentric rings"
13442
+ },
13443
+ stats: {
13444
+ fields: conceptFields(
13445
+ "stats",
13446
+ `items: ["Figure: what it measures", \u2026] ${conceptItemCount("stats")} such as "40%: of trips made on foot"`
13447
+ ),
13448
+ use: "several key numbers the {source} states, side by side; each counts up"
13449
+ },
13450
+ pyramid: {
13451
+ fields: conceptFields("pyramid"),
13452
+ use: "a hierarchy, or levels that rest on one another, drawn as a pyramid"
13453
+ },
13454
+ funnel: {
13455
+ fields: conceptFields("funnel"),
13456
+ use: "stages that narrow from many to few, drawn as a funnel"
13457
+ },
13458
+ hub: {
13459
+ fields: conceptFields("hub"),
13460
+ use: "factors, causes or parts of the one thing `center` names, drawn around a hub"
13461
+ },
13462
+ venn: {
13463
+ fields: conceptFields(
13464
+ "venn",
13465
+ 'items: ["A: what only A has", "B: what only B has", "Both: what they share"]'
13466
+ ),
13467
+ use: "two things compared by what each has alone and what they share, drawn as overlapping circles"
13468
+ },
13469
+ matrix: {
13470
+ fields: conceptFields("matrix"),
13471
+ use: "four ideas placed on two axes, such as effort against impact"
12038
13472
  }
12039
13473
  };
12040
- function guideText(text, wording = {}) {
12041
- return text.replace(/\{source\}/gu, wording.source ?? "source");
13474
+ function guideText(text2, wording = {}) {
13475
+ return text2.replace(/\{source\}/gu, wording.source ?? "source");
12042
13476
  }
12043
13477
  function templateCatalog(entries, wording = {}) {
12044
13478
  return entries.map((entry) => {
@@ -12059,13 +13493,51 @@ var PRESENTATION_LAYOUT_GUIDE = {
12059
13493
  comparison: "exactly 2 genuinely contrasting points",
12060
13494
  steps: "2\u20133 ordered actions",
12061
13495
  image: 'only when the current layout is "image"; the headline is then its caption',
12062
- cards: "2\u20134 parallel ideas side by side, such as kinds, causes or features",
12063
- process: "2\u20134 ordered stages or dated events, drawn as numbered steps joined by arrows",
12064
- cycle: "3\u20135 stages that repeat in a loop, drawn on a ring",
12065
- layers: "3\u20135 parts nested inside one another, innermost first, such as a core and the layers around it, drawn as concentric rings"
13496
+ cards: `${points("cards")} parallel ideas side by side, such as kinds, causes or features`,
13497
+ process: `${points("process")} ordered stages or dated events, drawn as numbered steps joined by arrows`,
13498
+ cycle: `${points("cycle")} stages that repeat in a loop, drawn on a ring`,
13499
+ layers: `${points("layers")} parts nested inside one another, innermost first, such as a core and the layers around it, drawn as concentric rings`,
13500
+ stats: `${points("stats")} key figures the passage states, each point the number itself, then a colon and what it measures, such as "40%: of trips made on foot" or "$3 million: raised"; they count up side by side`,
13501
+ pyramid: `${points("pyramid")} levels of a hierarchy, the top first and the base last, drawn as a pyramid`,
13502
+ funnel: `${points("funnel")} stages that narrow from many to few, the widest first, drawn as a funnel`,
13503
+ hub: `${points("hub")} factors, causes or parts of one thing, drawn around a hub; add "center", that thing in 1\u20133 words`,
13504
+ venn: `${points("venn")} points: two things compared, each "Name: what only it has", then "Both: what they share"; drawn as overlapping circles`,
13505
+ matrix: `${points("matrix")} points placed on two axes, in reading order: top left, top right, bottom left, bottom right; add "xAxis" and "yAxis", what grows from left to right and from bottom to top`
12066
13506
  };
12067
- var DRAWN = ["cards", "process", "cycle", "layers"];
12068
- var NUMBERS = ["statHighlight", "comparisonBar", "columnChart", "barChart", "pieChart"];
13507
+ var PRESENTATION_LAYOUT_LABELS = {
13508
+ title: { label: "Title", description: "Opens the deck or closes it." },
13509
+ divider: { label: "Section break", description: "A new topic begins; only the headline." },
13510
+ statement: { label: "Statement", description: "One idea under the headline." },
13511
+ emphasis: { label: "Big statement", description: "One short takeaway in display type." },
13512
+ quote: { label: "Quote", description: "One sentence in the passage\u2019s own words." },
13513
+ stat: { label: "Key number", description: "One figure, large, beside what it means." },
13514
+ definition: { label: "Definition", description: "A term and what it means." },
13515
+ list: { label: "Points", description: "Two or three points side by side." },
13516
+ comparison: { label: "Comparison", description: "Two sides set against each other." },
13517
+ steps: { label: "Steps", description: "Ordered actions, numbered and joined by arrows." },
13518
+ image: { label: "Picture", description: "A picture with the headline as its caption." },
13519
+ cards: { label: "Cards", description: "Parallel ideas side by side." },
13520
+ process: { label: "Process", description: "Ordered stages; dated ones draw as a timeline." },
13521
+ cycle: { label: "Cycle", description: "Stages on a ring that comes back to its start." },
13522
+ layers: { label: "Layers", description: "Nested parts as concentric rings." },
13523
+ stats: { label: "Key numbers", description: "Several figures side by side, counting up." },
13524
+ pyramid: { label: "Pyramid", description: "Levels of a hierarchy, the top first." },
13525
+ funnel: { label: "Funnel", description: "Stages that narrow from many to few." },
13526
+ hub: { label: "Hub", description: "Ideas around the one thing they share." },
13527
+ venn: { label: "Venn", description: "Two sets and what they share." },
13528
+ matrix: { label: "Matrix", description: "Four ideas on two axes." }
13529
+ };
13530
+ var DRAWN_CORE = ["cards", "process", "cycle", "layers"];
13531
+ var DRAWN_SHAPES = ["pyramid", "funnel", "hub", "venn", "matrix"];
13532
+ var DRAWN = [...DRAWN_CORE, ...DRAWN_SHAPES];
13533
+ var NUMBERS = [
13534
+ "statHighlight",
13535
+ "stats",
13536
+ "comparisonBar",
13537
+ "columnChart",
13538
+ "barChart",
13539
+ "pieChart"
13540
+ ];
12069
13541
  var ACCENTED = [
12070
13542
  "factCard",
12071
13543
  "statHighlight",
@@ -12081,7 +13553,9 @@ var AUTHORING_TRICKS = [
12081
13553
  },
12082
13554
  {
12083
13555
  id: "numbers-as-numbers",
12084
- text: "Draw numbers as numbers: one striking quantity \u2192 statHighlight (it counts up); two quantities in one unit \u2192 comparisonBar; three or more values in one unit \u2192 a chart; three to seven dated events \u2192 timeline.",
13556
+ text: "Draw numbers as numbers: one striking quantity \u2192 statHighlight (it counts up); two to four key quantities in different units \u2192 stats; two quantities in one unit \u2192 comparisonBar; three or more values in one unit \u2192 a chart; three to seven dated events \u2192 timeline.",
13557
+ summary: 'Draw numbers as numbers: when the passage turns on one figure choose "stat"; when it gives two to four key figures choose "stats" and start each point with its number, as in "40%: of trips made on foot".',
13558
+ storyboard: 'Draw numbers as numbers: a passage that turns on one figure is a "stat"; one that gives two to four key figures is "stats".',
12085
13559
  templates: [...NUMBERS, "timeline"]
12086
13560
  },
12087
13561
  {
@@ -12092,8 +13566,16 @@ var AUTHORING_TRICKS = [
12092
13566
  {
12093
13567
  id: "draw-ideas",
12094
13568
  text: `Slides that draw their ideas explain best: several parallel ideas \u2192 cards, ordered stages \u2192 process, a loop that comes back to its start \u2192 cycle, parts nested inside one another \u2192 layers; prefer them to a list. Write each item as "Label: detail", a label of 1\u20134 words and a detail of at most 70 characters, in the {source}'s own words. For dated events, start each label with its date: a process of dated stages draws as a timeline.`,
12095
- summary: `Slides that draw their ideas explain best: when the passage has several parallel ideas, ordered stages, a repeating loop or nested parts, choose ${DRAWN.map((layout) => `"${layout}"`).join(", ")} rather than "statement" or "list", and write each of their points (up to 4) as "Label: detail", a label of 1\u20134 words and a detail of at most 70 characters. For dated events, start each label with its date.`,
12096
- templates: DRAWN
13569
+ summary: `Slides that draw their ideas explain best: when the passage has several parallel ideas, ordered stages, a repeating loop or nested parts, choose ${DRAWN_CORE.map((layout) => `"${layout}"`).join(", ")} rather than "statement" or "list", and write each of their points (up to 4) as "Label: detail", a label of 1\u20134 words and a detail of at most 70 characters. For dated events, start each label with its date.`,
13570
+ storyboard: 'Draw ideas rather than list them: several parallel ideas are "cards", ordered stages or dated events a "process", a loop that comes back to its start a "cycle", and parts nested inside one another "layers".',
13571
+ templates: DRAWN_CORE
13572
+ },
13573
+ {
13574
+ id: "draw-shapes",
13575
+ text: 'Some relationships have a shape of their own: a hierarchy or levels that rest on one another \u2192 pyramid (top first); stages that narrow from many to few \u2192 funnel (widest first); factors, causes or parts of one thing \u2192 hub, with that thing as `center`; two things and what they share \u2192 venn, its last item "Both: \u2026"; four ideas on two axes \u2192 matrix. Choose one only when the {source} states that relationship; a shape the ideas do not have misleads.',
13576
+ summary: 'Some relationships have a shape of their own: a hierarchy \u2192 "pyramid", stages that narrow from many to few \u2192 "funnel", factors or causes of one thing \u2192 "hub", two things and what they share \u2192 "venn" (its third point "Both: \u2026"), four ideas on two axes \u2192 "matrix". Choose one only when the passage states that relationship; a shape the ideas do not have misleads.',
13577
+ storyboard: 'Some relationships have a shape of their own: a hierarchy is a "pyramid", stages that narrow from many to few a "funnel", factors or causes of one thing a "hub", two things and what they share a "venn", and four ideas on two axes a "matrix". Choose one only when the passage states that relationship.',
13578
+ templates: DRAWN_SHAPES
12097
13579
  },
12098
13580
  {
12099
13581
  id: "reveal-as-spoken",
@@ -12117,7 +13599,8 @@ var AUTHORING_TRICKS = [
12117
13599
  },
12118
13600
  {
12119
13601
  id: "vary",
12120
- text: "Vary the slides: when two templates fit equally well, choose the one not just shown. A long run of photo-and-caption slides is where a narrated story feels static."
13602
+ text: "Vary the slides: when two templates fit equally well, choose the one not just shown. A long run of photo-and-caption slides is where a narrated story feels static.",
13603
+ storyboard: "Vary the deck: no layout on more than a third of the slides, and no two neighbouring slides alike when another layout fits as well."
12121
13604
  }
12122
13605
  ];
12123
13606
  function authoringTricks(options = {}) {
@@ -12126,8 +13609,8 @@ function authoringTricks(options = {}) {
12126
13609
  return selected.filter(
12127
13610
  (trick) => !offered || !trick.templates || trick.templates.some((template) => offered.has(template))
12128
13611
  ).flatMap((trick) => {
12129
- const text = options.surface === "summary" ? trick.summary : trick.text;
12130
- return text ? [guideText(text, options.wording)] : [];
13612
+ const text2 = options.surface === "summary" ? trick.summary : options.surface === "storyboard" ? trick.storyboard : trick.text;
13613
+ return text2 ? [guideText(text2, options.wording)] : [];
12131
13614
  });
12132
13615
  }
12133
13616
 
@@ -12157,10 +13640,10 @@ function coverScrimGradient(background) {
12157
13640
  const stops = COVER_SCRIM_STOPS.map((s) => `${withAlpha(background, s.alpha)} ${s.pos}%`);
12158
13641
  return `linear-gradient(0deg, ${stops.join(", ")})`;
12159
13642
  }
12160
- function fitFrostedText(text, size, minSize, maxPlateWidth, maxLines) {
13643
+ function fitFrostedText(text2, size, minSize, maxPlateWidth, maxLines) {
12161
13644
  const wrapAt = (s) => {
12162
13645
  const column = maxPlateWidth - 2 * FROSTED_PAD_X_EM * s;
12163
- const lines = text.split("\n").flatMap((line) => balancedDisplayLines(line, s, column));
13646
+ const lines = text2.split("\n").flatMap((line) => balancedDisplayLines(line, s, column));
12164
13647
  const width = Math.max(...lines.map((line) => estimateDisplayTextWidth(line, s)));
12165
13648
  return { lines, width, column };
12166
13649
  };
@@ -12658,6 +14141,166 @@ function expandCoverBlock(config, context, template = "cover") {
12658
14141
  return coverBlock(input, context);
12659
14142
  }
12660
14143
 
14144
+ // src/doc/templates/markdownShapeGuide.ts
14145
+ var MARKDOWN_SHAPE_EXAMPLES = [
14146
+ {
14147
+ template: "process",
14148
+ cue: "a numbered list",
14149
+ brief: ["a numbered list", "steps"],
14150
+ drawn: "numbered steps joined by arrows",
14151
+ heading: "How a release ships",
14152
+ ordered: true,
14153
+ items: ["Draft the release notes", "Tag and test the build", "Roll out to everyone"]
14154
+ },
14155
+ {
14156
+ template: "cards",
14157
+ cue: "items written `Label: detail`",
14158
+ brief: ["`Label: detail` items", "cards"],
14159
+ drawn: "cards side by side",
14160
+ heading: "Three choices for next quarter",
14161
+ items: [
14162
+ "Fund: redesign permissions setup",
14163
+ "Protect: keep the activation gains",
14164
+ "Defer: advanced mobile editing"
14165
+ ]
14166
+ },
14167
+ {
14168
+ template: "stats",
14169
+ cue: "items written `Figure: what it measures`",
14170
+ brief: ["`Figure: what it measures` items", "key numbers"],
14171
+ drawn: "key numbers that count up",
14172
+ heading: "The quarter in numbers",
14173
+ items: ["18,420: active teams", "71%: weekly retained teams", "$3.8M: expansion revenue"]
14174
+ },
14175
+ {
14176
+ template: "process",
14177
+ cue: "items that open with dates in order",
14178
+ brief: ["items that open with dates", "a timeline"],
14179
+ drawn: "a timeline",
14180
+ heading: "Our story",
14181
+ items: [
14182
+ "2016: founded in a Portland garage",
14183
+ "2018: first product ships",
14184
+ "2022: Berlin office opens"
14185
+ ]
14186
+ },
14187
+ {
14188
+ template: "cycle",
14189
+ cue: "a heading that says cycle or loop",
14190
+ brief: ["a cycle or loop heading", "a ring"],
14191
+ drawn: "stages on a ring",
14192
+ heading: "The water cycle",
14193
+ items: [
14194
+ "Evaporation: the sun lifts water from the sea",
14195
+ "Condensation: vapour cools into cloud",
14196
+ "Precipitation: rain and snow fall"
14197
+ ]
14198
+ },
14199
+ {
14200
+ template: "layers",
14201
+ cue: "a heading that says layers or anatomy, with the innermost item first",
14202
+ brief: ["a layers or anatomy heading (innermost first)", "rings"],
14203
+ drawn: "concentric rings",
14204
+ heading: "Layers of the Earth",
14205
+ items: [
14206
+ "Inner core: solid iron",
14207
+ "Outer core: liquid iron",
14208
+ "Mantle: slowly flowing rock",
14209
+ "Crust: the thin outer layer"
14210
+ ]
14211
+ },
14212
+ {
14213
+ template: "pyramid",
14214
+ cue: "a heading that says hierarchy, pyramid, tiers or chain of command, with the top item first",
14215
+ brief: ["a hierarchy or pyramid heading (top first)", "a pyramid"],
14216
+ drawn: "a pyramid",
14217
+ heading: "The chain of command",
14218
+ items: [
14219
+ "Chief executive: sets the direction",
14220
+ "Vice presidents: turn it into plans",
14221
+ "Team leads: run the daily work",
14222
+ "Engineers: build the product"
14223
+ ]
14224
+ },
14225
+ {
14226
+ template: "funnel",
14227
+ cue: "a heading that says funnel or conversion, with the widest item first",
14228
+ brief: ["a funnel or conversion heading (widest first)", "a funnel"],
14229
+ drawn: "a funnel",
14230
+ heading: "The sign-up funnel",
14231
+ items: ["48,000: visit the site", "6,200: start a trial", "640: become customers"]
14232
+ },
14233
+ {
14234
+ template: "hub",
14235
+ cue: "a heading that names factors, causes, drivers or pillars over `Label: detail` items",
14236
+ brief: ["a factors, causes or drivers heading", "a hub"],
14237
+ drawn: "ideas around a hub",
14238
+ heading: "Three factors behind every delay",
14239
+ items: [
14240
+ "Scope: requirements keep changing",
14241
+ "Staffing: too few senior engineers",
14242
+ "Funding: released two months late"
14243
+ ],
14244
+ inputs: { center: "Delays" }
14245
+ },
14246
+ {
14247
+ template: "venn",
14248
+ cue: "two `Name: \u2026` items and a third `Both: \u2026`",
14249
+ brief: ["two items and a `Both: \u2026` item", "a Venn diagram"],
14250
+ drawn: "a Venn diagram",
14251
+ heading: "Designers and engineers",
14252
+ items: [
14253
+ "Designers: research and the interface",
14254
+ "Engineers: architecture and the code",
14255
+ "Both: accessibility and testing"
14256
+ ]
14257
+ },
14258
+ {
14259
+ template: "matrix",
14260
+ cue: "exactly four items under a heading that says matrix or quadrants (top left, top right, bottom left, bottom right)",
14261
+ brief: ["four items under a matrix or quadrants heading", "a two-by-two"],
14262
+ drawn: "a two-by-two",
14263
+ heading: "The effort and impact matrix",
14264
+ items: [
14265
+ "Quick wins: little effort, high impact",
14266
+ "Big bets: high effort, high impact",
14267
+ "Fill-ins: little effort, low impact",
14268
+ "Time sinks: high effort, low impact"
14269
+ ],
14270
+ inputs: { xAxis: "Effort", yAxis: "Impact" }
14271
+ },
14272
+ {
14273
+ template: "cards",
14274
+ cue: "two items under an `A vs B` heading",
14275
+ brief: ["two items under an `A vs B` heading", "two panels"],
14276
+ drawn: "two panels set against each other",
14277
+ heading: "Self-hosting vs a managed service",
14278
+ items: ["Self-hosting: costs less, needs an engineer", "Managed: costs more, includes backups"]
14279
+ }
14280
+ ];
14281
+ function markdownShapeExample(template, options = {}) {
14282
+ const example = MARKDOWN_SHAPE_EXAMPLES.find((entry) => entry.template === template);
14283
+ const hashes = "#".repeat(Math.min(6, Math.max(1, options.headingLevel ?? 2)));
14284
+ const inputs = Object.entries(example.inputs ?? {}).map(([key, value]) => ` ${key}="${value}"`).join("");
14285
+ return [
14286
+ `${hashes} ${example.heading} {[${template}${inputs}]}`,
14287
+ "",
14288
+ ...example.items.map((item, index) => `${example.ordered ? `${index + 1}.` : "-"} ${item}`)
14289
+ ].join("\n");
14290
+ }
14291
+ function markdownShapeGuide() {
14292
+ return [
14293
+ "A section whose body is one list of 2\u20136 short items is drawn as a picture of how they relate, with no annotation. What decides the picture:",
14294
+ ...MARKDOWN_SHAPE_EXAMPLES.map((example) => `- ${example.cue} \u2192 ${example.drawn}`),
14295
+ `To choose a picture yourself, annotate the heading: \`## Title {[funnel]}\`. A hub takes \`center="\u2026"\` (1\u20133 words) and a matrix \`xAxis="\u2026" yAxis="\u2026"\`; a venn is exactly two sets and one \`Both: \u2026\` item, and a matrix ${conceptItemCount("matrix")} items.`,
14296
+ "Only a list that is the whole body is drawn: a paragraph beside it, a link, an image or a seventh item keeps the section as written. Put an introduction in the heading or on the slide before. Name a shape only when the items really have it; a plain list is better than a misleading drawing."
14297
+ ];
14298
+ }
14299
+ function markdownShapeSummary() {
14300
+ const rules = MARKDOWN_SHAPE_EXAMPLES.map(({ brief }) => `${brief[0]} \u2192 ${brief[1]}`).join("; ");
14301
+ return `A section whose body is one list of 2\u20136 short items is drawn as a picture of how they relate, with no annotation: ${rules}. Only a list that is the whole body is drawn, so keep any introduction in the heading or on the slide before; to choose a picture yourself, annotate the heading (\`# Title {[funnel]}\`).`;
14302
+ }
14303
+
12661
14304
  // src/doc/templates/index.ts
12662
14305
  function materializeScheduledBlock(sourceBlock, blockIndex, totalBlocks, theme, viewport, persistentLayers, expandedPersistentLayers, registry, failureMode, onDiagnostic, motion, mediaAspectRatios) {
12663
14306
  const materialized = materializeBlockLayersWithRuntime(
@@ -12684,7 +14327,11 @@ function materializeScheduledBlock(sourceBlock, blockIndex, totalBlocks, theme,
12684
14327
  audioSegment: sourceBlock.audioSegment,
12685
14328
  ...materialized.layers.length > 0 ? { layers: materialized.layers } : {},
12686
14329
  transition: sourceBlock.transition,
12687
- template: sourceBlock.template,
14330
+ // A block its template could not draw was drawn as content. Name what
14331
+ // drew it, so renderers size its text as a content slide's: they skip
14332
+ // grow-to-fit by template name, and "comparisonBar" would keep a
14333
+ // paragraph at chart-label size.
14334
+ template: materialized.diagnostic?.code === "template-drew-nothing" ? "content" : sourceBlock.template,
12688
14335
  build: sourceBlock.build,
12689
14336
  buildTiming: sourceBlock.buildTiming,
12690
14337
  buildSource: sourceBlock.buildSource,
@@ -12896,11 +14543,12 @@ function expandDocBlocks(blocks, options = {}) {
12896
14543
  }
12897
14544
  }
12898
14545
  const MIN_TRANSITION_GAP = 5;
14546
+ const GAP_TOLERANCE = 1e-3;
12899
14547
  const segmentEnd = audioSegment.startTime + audioSegment.duration;
12900
14548
  while (segmentExpandedBlocks.length > 1) {
12901
14549
  const lastBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 1];
12902
14550
  const timeFromLastToEnd = segmentEnd - lastBlock.startTime;
12903
- if (mergeShortBlocks && timeFromLastToEnd < MIN_TRANSITION_GAP && lastBlock.template !== "sectionHeader") {
14551
+ if (mergeShortBlocks && timeFromLastToEnd < MIN_TRANSITION_GAP - GAP_TOLERANCE && lastBlock.template !== "sectionHeader") {
12904
14552
  const prevBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 2];
12905
14553
  prevBlock.duration = segmentEnd - prevBlock.startTime;
12906
14554
  lastBlock.duration = 0;
@@ -12924,7 +14572,7 @@ function expandDocBlocks(blocks, options = {}) {
12924
14572
  changed = false;
12925
14573
  for (let i = segmentExpandedBlocks.length - 1; i >= 1; i--) {
12926
14574
  const block = segmentExpandedBlocks[i];
12927
- if (block.duration > 0 && block.duration < MIN_TRANSITION_GAP) {
14575
+ if (block.duration > 0 && block.duration < MIN_TRANSITION_GAP - GAP_TOLERANCE) {
12928
14576
  const prev = segmentExpandedBlocks[i - 1];
12929
14577
  const blockEnd = block.startTime + block.duration;
12930
14578
  prev.duration = blockEnd - prev.startTime;
@@ -12943,7 +14591,7 @@ function expandDocBlocks(blocks, options = {}) {
12943
14591
  if (block.duration > MAX_BLOCK_DURATION) {
12944
14592
  const numParts = Math.ceil(block.duration / MAX_BLOCK_DURATION);
12945
14593
  const partDuration = block.duration / numParts;
12946
- if (partDuration >= MIN_TRANSITION_GAP) {
14594
+ if (partDuration >= MIN_TRANSITION_GAP - GAP_TOLERANCE) {
12947
14595
  block.duration = partDuration;
12948
14596
  const parts = [];
12949
14597
  for (let p = 1; p < numParts; p++) {
@@ -13112,20 +14760,78 @@ function detectPromotableBodyAnnotation(contents) {
13112
14760
  return { data, strippedContents };
13113
14761
  }
13114
14762
 
14763
+ // src/doc/videoTiming.ts
14764
+ function clipEndsWithBlock(clip) {
14765
+ return clip.kind === "video" && clip.anchor === "block" && clip.spillover !== true && clip.lockToBlock !== false;
14766
+ }
14767
+ function blockVideos(block) {
14768
+ const body = extractEmbeddedVideos(block.contents).map((video) => ({
14769
+ src: video.src,
14770
+ startAt: video.startAt,
14771
+ clipStart: video.clipStart,
14772
+ clipEnd: video.clipEnd,
14773
+ cuts: video.cuts
14774
+ }));
14775
+ const clips = (block.media ?? []).filter(clipEndsWithBlock).map((clip) => ({
14776
+ src: clip.src,
14777
+ startAt: clip.startAt,
14778
+ clipStart: clip.clipStart,
14779
+ clipEnd: clip.clipEnd,
14780
+ cuts: clip.edits?.cuts
14781
+ }));
14782
+ return [...body, ...clips];
14783
+ }
14784
+ function knownLength(seconds) {
14785
+ return seconds != null && Number.isFinite(seconds) && seconds > 0 ? seconds : void 0;
14786
+ }
14787
+ function videoEnd(video, lookup) {
14788
+ const sourceDuration = knownLength(lookup?.(video.src));
14789
+ if (video.clipEnd == null && sourceDuration == null) return null;
14790
+ const played = createMediaTimeMap({
14791
+ clipStart: video.clipStart,
14792
+ clipEnd: video.clipEnd,
14793
+ cuts: video.cuts,
14794
+ sourceDuration
14795
+ }).playedDuration;
14796
+ if (!Number.isFinite(played) || played <= 0) return null;
14797
+ return Math.max(0, video.startAt ?? 0) + played;
14798
+ }
14799
+ function blockVideoLength(block, lookup) {
14800
+ let longest = null;
14801
+ for (const video of blockVideos(block)) {
14802
+ const end = videoEnd(video, lookup);
14803
+ if (end != null && (longest == null || end > longest)) longest = end;
14804
+ }
14805
+ return longest;
14806
+ }
14807
+ function collectVideoDurationSources(doc) {
14808
+ const sources = /* @__PURE__ */ new Set();
14809
+ const visit = (blocks) => {
14810
+ for (const block of blocks) {
14811
+ for (const video of blockVideos(block)) {
14812
+ if (video.clipEnd == null && video.src) sources.add(video.src);
14813
+ }
14814
+ if (block.children?.length) visit(block.children);
14815
+ }
14816
+ };
14817
+ visit(doc.blocks);
14818
+ return [...sources];
14819
+ }
14820
+
13115
14821
  // src/doc/markdownToDoc.ts
13116
14822
  var SLUG_NON_WORD_RE = /[^\w\s-]/g;
13117
14823
  var SLUG_SPACES_RE = /[\s_]+/g;
13118
14824
  var SLUG_MULTI_HYPHEN_RE = /-+/g;
13119
14825
  var SLUG_TRIM_HYPHEN_RE = /^-|-$/g;
13120
- function slugify(text) {
13121
- return text.toLowerCase().replace(SLUG_NON_WORD_RE, "").replace(SLUG_SPACES_RE, "-").replace(SLUG_MULTI_HYPHEN_RE, "-").replace(SLUG_TRIM_HYPHEN_RE, "") || // Trim leading/trailing hyphens
14826
+ function slugify(text2) {
14827
+ return text2.toLowerCase().replace(SLUG_NON_WORD_RE, "").replace(SLUG_SPACES_RE, "-").replace(SLUG_MULTI_HYPHEN_RE, "-").replace(SLUG_TRIM_HYPHEN_RE, "") || // Trim leading/trailing hyphens
13122
14828
  "block";
13123
14829
  }
13124
14830
  function createIdGenerator() {
13125
14831
  const used = /* @__PURE__ */ new Map();
13126
14832
  const generate = (heading, _index) => {
13127
- const text = extractPlainText(heading);
13128
- const base = slugify(text);
14833
+ const text2 = extractPlainText(heading);
14834
+ const base = slugify(text2);
13129
14835
  const count = used.get(base) ?? 0;
13130
14836
  used.set(base, count + 1);
13131
14837
  if (count === 0) return base;
@@ -13134,8 +14840,8 @@ function createIdGenerator() {
13134
14840
  generate.reserve = (id) => {
13135
14841
  used.set(id, Math.max(used.get(id) ?? 0, 1));
13136
14842
  };
13137
- generate.fromText = (text) => {
13138
- const base = slugify(text);
14843
+ generate.fromText = (text2) => {
14844
+ const base = slugify(text2);
13139
14845
  const count = used.get(base) ?? 0;
13140
14846
  used.set(base, count + 1);
13141
14847
  return count === 0 ? base : `${base}-${count + 1}`;
@@ -13340,10 +15046,15 @@ function markdownToDoc(markdownDoc, options) {
13340
15046
  applyTimelineData(block, diagnostics);
13341
15047
  applyTreeData(block, diagnostics);
13342
15048
  }
13343
- if ((options?.autoTemplates ?? true) && !frontmatterDisablesAutoTemplates(markdownDoc)) {
13344
- applyAutoTemplates(rootBlocks, resolveTemplateName(defaultTemplate), { featureIndex: 0 });
15049
+ const autoLevel = frontmatterAutoTemplates(markdownDoc, options?.autoTemplates ?? true);
15050
+ if (autoLevel) {
15051
+ applyAutoTemplates(rootBlocks, resolveTemplateName(defaultTemplate), {
15052
+ featureIndex: 0,
15053
+ concepts: autoLevel !== "structural"
15054
+ });
13345
15055
  }
13346
15056
  for (const block of allBlocks) collectTimelineWarnings(block, diagnostics);
15057
+ for (const block of allBlocks) collectShapeWarning(block, diagnostics);
13347
15058
  const seenIds = /* @__PURE__ */ new Map();
13348
15059
  for (const block of allBlocks) {
13349
15060
  if (seenIds.has(block.id)) {
@@ -13372,46 +15083,13 @@ function markdownToDoc(markdownDoc, options) {
13372
15083
  }
13373
15084
  }
13374
15085
  const minDuration = 3;
13375
- const phrases = [];
13376
15086
  for (const block of allBlocks) {
13377
15087
  if (pinnedMeta(block).duration != null) continue;
13378
15088
  const bodyText3 = getBlockBodyText2(block);
13379
- if (bodyText3.length > 0) {
13380
- const estimate = estimateReadingTime(bodyText3);
13381
- block.duration = Math.max(minDuration, estimate.seconds);
13382
- } else {
13383
- block.duration = defaultDuration;
13384
- }
13385
- }
13386
- let currentTime = 0;
13387
- for (const block of allBlocks) {
13388
- const explicitStart = pinnedMeta(block).startTime;
13389
- if (explicitStart == null) {
13390
- block.startTime = currentTime;
13391
- }
13392
- const bodyText3 = getBlockBodyText2(block);
13393
- if (bodyText3.length > 0) {
13394
- const sentences = splitIntoSentences(bodyText3);
13395
- if (sentences.length > 0) {
13396
- const timePerSentence = block.duration / sentences.length;
13397
- for (let i = 0; i < sentences.length; i++) {
13398
- phrases.push({
13399
- text: sentences[i],
13400
- startTime: currentTime + i * timePerSentence,
13401
- endTime: currentTime + (i + 1) * timePerSentence,
13402
- audioSegment: 0
13403
- });
13404
- }
13405
- }
13406
- }
13407
- currentTime += block.duration;
13408
- }
13409
- const blockById = new Map(allBlocks.map((block) => [block.id, block]));
13410
- for (const clip of documentMedia) {
13411
- if (clip.lockToBlock === false && clip.origin?.format === "html" && !/\bdata-squisq-video-start-at\s*=/i.test(clip.origin.raw ?? "")) {
13412
- clip.startAt = blockById.get(clip.origin.blockId)?.startTime ?? 0;
13413
- }
15089
+ const estimate = bodyText3.length > 0 ? Math.max(minDuration, estimateReadingTime(bodyText3).seconds) : defaultDuration;
15090
+ block.duration = Math.max(estimate, blockVideoLength(block) ?? 0);
13414
15091
  }
15092
+ const { phrases, duration: totalDuration } = sequenceBlockTimeline(allBlocks, documentMedia);
13415
15093
  const captions = phrases.length > 0 ? {
13416
15094
  phrases,
13417
15095
  ...options?.captionsGeneratedAt ? { generatedAt: options.captionsGeneratedAt } : {},
@@ -13423,7 +15101,7 @@ function markdownToDoc(markdownDoc, options) {
13423
15101
  const motion = readFrontmatterMotion(markdownDoc.frontmatter);
13424
15102
  const doc = {
13425
15103
  articleId,
13426
- duration: currentTime,
15104
+ duration: totalDuration,
13427
15105
  blocks: rootBlocks,
13428
15106
  audio: {
13429
15107
  segments: []
@@ -13445,6 +15123,64 @@ function markdownToDoc(markdownDoc, options) {
13445
15123
  }
13446
15124
  return doc;
13447
15125
  }
15126
+ function sequenceBlockTimeline(blocks, documentMedia) {
15127
+ const phrases = [];
15128
+ let currentTime = 0;
15129
+ for (const block of blocks) {
15130
+ const explicitStart = pinnedMeta(block).startTime;
15131
+ if (explicitStart == null) {
15132
+ block.startTime = currentTime;
15133
+ }
15134
+ const bodyText3 = getBlockBodyText2(block);
15135
+ if (bodyText3.length > 0) {
15136
+ const sentences = splitIntoSentences(bodyText3);
15137
+ if (sentences.length > 0) {
15138
+ const timePerSentence = block.duration / sentences.length;
15139
+ for (let i = 0; i < sentences.length; i++) {
15140
+ phrases.push({
15141
+ text: sentences[i],
15142
+ startTime: currentTime + i * timePerSentence,
15143
+ endTime: currentTime + (i + 1) * timePerSentence,
15144
+ audioSegment: 0
15145
+ });
15146
+ }
15147
+ }
15148
+ }
15149
+ currentTime += block.duration;
15150
+ }
15151
+ const blockById = new Map(blocks.map((block) => [block.id, block]));
15152
+ for (const clip of documentMedia) {
15153
+ if (clip.lockToBlock === false && clip.origin?.format === "html" && !/\bdata-squisq-video-start-at\s*=/i.test(clip.origin.raw ?? "")) {
15154
+ clip.startAt = blockById.get(clip.origin.blockId)?.startTime ?? 0;
15155
+ }
15156
+ }
15157
+ return { phrases, duration: currentTime };
15158
+ }
15159
+ var VIDEO_FLOOR_EPSILON = 1e-3;
15160
+ function applyVideoDurations(doc, lookup) {
15161
+ const grownBy = /* @__PURE__ */ new Map();
15162
+ for (const block of flattenBlocks(doc.blocks)) {
15163
+ if (pinnedMeta(block).duration != null) continue;
15164
+ const floor = blockVideoLength(block, lookup);
15165
+ if (floor != null && floor > block.duration + VIDEO_FLOOR_EPSILON) grownBy.set(block, floor);
15166
+ }
15167
+ if (grownBy.size === 0) return doc;
15168
+ const copy = (blocks2) => blocks2.map((block) => ({
15169
+ ...block,
15170
+ ...grownBy.has(block) ? { duration: grownBy.get(block) } : {},
15171
+ ...block.children ? { children: copy(block.children) } : {}
15172
+ }));
15173
+ const blocks = copy(doc.blocks);
15174
+ const documentMedia = doc.documentMedia?.map((clip) => ({ ...clip }));
15175
+ const { phrases, duration } = sequenceBlockTimeline(flattenBlocks(blocks), documentMedia ?? []);
15176
+ return {
15177
+ ...doc,
15178
+ blocks,
15179
+ duration,
15180
+ ...doc.captions ? { captions: { ...doc.captions, phrases } } : {},
15181
+ ...documentMedia ? { documentMedia } : {}
15182
+ };
15183
+ }
13448
15184
  function flattenBlocks(blocks) {
13449
15185
  const result = [];
13450
15186
  for (const block of blocks) {
@@ -13561,6 +15297,23 @@ function applyTimelineData(block, diagnostics) {
13561
15297
  }
13562
15298
  block.templateData = { ...derived, ...block.templateData };
13563
15299
  }
15300
+ function collectShapeWarning(block, diagnostics) {
15301
+ if (!block.sourceHeading?.templateAnnotation?.template && !block.promotedBodyAnnotation) return;
15302
+ const template = conceptTemplate(resolveTemplateName(block.template ?? ""));
15303
+ if (!template) return;
15304
+ const given = block.templateData?.items;
15305
+ const items = Array.isArray(given) ? given : extractConceptItems(block.contents);
15306
+ if (!items.length) return;
15307
+ const downgrade = conceptDowngrade(template, items);
15308
+ if (!downgrade) return;
15309
+ diagnostics.push({
15310
+ severity: "warning",
15311
+ code: "template-shape-unmet",
15312
+ message: `Block "${block.id}" asks for ${template} but is drawn as ${downgrade.drawsAs === "list" ? "a list" : downgrade.drawsAs}: ${downgrade.reason}`,
15313
+ blockId: block.id,
15314
+ ...lineOf(block)
15315
+ });
15316
+ }
13564
15317
  function collectTimelineWarnings(block, diagnostics) {
13565
15318
  if (resolveTemplateName(block.template ?? "") !== "timeline") return;
13566
15319
  if (!block.templateData || !("tracks" in block.templateData)) return;
@@ -13578,20 +15331,26 @@ function collectTimelineWarnings(block, diagnostics) {
13578
15331
  });
13579
15332
  }
13580
15333
  }
13581
- function frontmatterDisablesAutoTemplates(markdownDoc) {
15334
+ function frontmatterAutoTemplates(markdownDoc, option) {
15335
+ if (!option) return false;
13582
15336
  const v = markdownDoc.frontmatter?.["squisq-auto-templates"];
13583
- return v === false || v === "false" || v === "off" || v === "no" || v === 0;
15337
+ if (v === false || v === "false" || v === "off" || v === "no" || v === 0) return false;
15338
+ if (v === "structural" || v === "basic") return "structural";
15339
+ return option;
13584
15340
  }
13585
15341
  function applyAutoTemplates(blocks, resolvedDefault, state) {
13586
15342
  for (const block of blocks) {
13587
15343
  const annotated = !!block.sourceHeading?.templateAnnotation?.template || !!block.promotedBodyAnnotation;
13588
15344
  if (block.sourceHeading && !annotated && block.template === resolvedDefault) {
13589
- const profile = profileBlockContents(block.contents ?? []);
13590
- const picked = pickAutoTemplate(profile, state.featureIndex);
15345
+ const profile = profileBlockContents(block.contents ?? [], { heading: block.title });
15346
+ let picked = pickAutoTemplate(profile, state.featureIndex, { concepts: state.concepts });
15347
+ const drawn = !!picked && picked !== "list" && profile.listShape?.template === picked;
15348
+ if (drawn && !autoTemplatePreservesContent(picked, block.contents)) picked = "list";
13591
15349
  if (picked && autoTemplatePreservesContent(picked, block.contents)) {
13592
- const inputs = deriveTemplateInputs(picked, block.title ?? "", block.contents, {
15350
+ const derived = deriveTemplateInputs(picked, block.title ?? "", block.contents, {
13593
15351
  preserveSourceHeading: true
13594
15352
  });
15353
+ const inputs = derived && drawn && picked !== "list" ? { ...derived, ...profile.listShape?.inputs } : derived;
13595
15354
  if (inputs) {
13596
15355
  if (picked === "leftFeature" || picked === "rightFeature") state.featureIndex += 1;
13597
15356
  block.template = picked;
@@ -13612,8 +15371,8 @@ function getBlockBodyText2(block) {
13612
15371
  return block.contents.filter((node) => !(consumedFence && node.type === "code")).filter((node) => !isMaterializedMediaNode(node)).map((node) => extractPlainText(node)).join("\n").trim();
13613
15372
  }
13614
15373
  var MAX_PHRASE_WORDS = 12;
13615
- function splitIntoSentences(text) {
13616
- const lines = text.split(/\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
15374
+ function splitIntoSentences(text2) {
15375
+ const lines = text2.split(/\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
13617
15376
  const sentenceRe = /(?<=[.!?])\s+/;
13618
15377
  const fragments = [];
13619
15378
  for (const line of lines) {
@@ -13785,12 +15544,12 @@ function buildNarrationScript(doc, options) {
13785
15544
  const separator = options?.blockSeparator ?? "\n\n";
13786
15545
  const pieces = [];
13787
15546
  for (const block of flattenRenderableBlocks(doc.blocks)) {
13788
- const text = blockSpokenText(block, includeTitles);
13789
- if (!text) continue;
15547
+ const text2 = blockSpokenText(block, includeTitles);
15548
+ if (!text2) continue;
13790
15549
  pieces.push({
13791
15550
  blockId: block.id,
13792
15551
  heading: block.title || void 0,
13793
- text
15552
+ text: text2
13794
15553
  });
13795
15554
  }
13796
15555
  let sourceText = "";
@@ -13831,20 +15590,20 @@ function buildNarrationScript(doc, options) {
13831
15590
  spoken
13832
15591
  });
13833
15592
  }
13834
- for (const range of blocks) {
13835
- range.tokenStart = tokens.length;
13836
- range.tokenEnd = 0;
15593
+ for (const range2 of blocks) {
15594
+ range2.tokenStart = tokens.length;
15595
+ range2.tokenEnd = 0;
13837
15596
  }
13838
15597
  for (let i = 0; i < tokens.length; i++) {
13839
- const range = blocks[tokens[i].blockIndex];
13840
- if (range) {
13841
- if (i < range.tokenStart) range.tokenStart = i;
13842
- range.tokenEnd = i + 1;
15598
+ const range2 = blocks[tokens[i].blockIndex];
15599
+ if (range2) {
15600
+ if (i < range2.tokenStart) range2.tokenStart = i;
15601
+ range2.tokenEnd = i + 1;
13843
15602
  }
13844
15603
  }
13845
- for (const range of blocks) {
13846
- if (range.tokenStart > range.tokenEnd) {
13847
- range.tokenStart = range.tokenEnd;
15604
+ for (const range2 of blocks) {
15605
+ if (range2.tokenStart > range2.tokenEnd) {
15606
+ range2.tokenStart = range2.tokenEnd;
13848
15607
  }
13849
15608
  }
13850
15609
  for (let i = 0; i < tokens.length; i++) {
@@ -13943,9 +15702,9 @@ function planContentBuilds(nodes, spec) {
13943
15702
  const steps = [];
13944
15703
  const targets = /* @__PURE__ */ new Map();
13945
15704
  if (!spec || spec.mode === "off") return { steps, targets };
13946
- const add = (node, text, ancestors) => {
15705
+ const add = (node, text2, ancestors) => {
13947
15706
  const index = steps.length;
13948
- steps.push({ id: `build-${index}`, text, ...ancestors.length ? { ancestors } : {} });
15707
+ steps.push({ id: `build-${index}`, text: text2, ...ancestors.length ? { ancestors } : {} });
13949
15708
  targets.set(node, index);
13950
15709
  return index;
13951
15710
  };
@@ -14067,26 +15826,26 @@ function resolveDocBuildTiming(doc, blocks) {
14067
15826
  });
14068
15827
  return { ...block, buildTiming: { sourceText: buildSourceText(steps), starts: starts2 } };
14069
15828
  }
14070
- const range = script?.blocks.find(
14071
- (range2) => range2.blockId === (block.sourceBlockId ?? block.id)
15829
+ const range2 = script?.blocks.find(
15830
+ (range3) => range3.blockId === (block.sourceBlockId ?? block.id)
14072
15831
  );
14073
- const original = range ? flattenRenderableBlocks(doc.blocks).find((source2) => source2.id === range.blockId) : void 0;
15832
+ const original = range2 ? flattenRenderableBlocks(doc.blocks).find((source2) => source2.id === range2.blockId) : void 0;
14074
15833
  const body = original ? getBlockBodyText2(original) : "";
14075
- const charStart = range ? body ? range.charEnd - body.length : range.charStart : 0;
14076
- const source = block.buildSource ?? (range ? { text: narration.sourceText.slice(charStart, range.charEnd), charStart } : void 0);
15834
+ const charStart = range2 ? body ? range2.charEnd - body.length : range2.charStart : 0;
15835
+ const source = block.buildSource ?? (range2 ? { text: narration.sourceText.slice(charStart, range2.charEnd), charStart } : void 0);
14077
15836
  if (!source) return block;
14078
15837
  let cursor = 0;
14079
15838
  const starts = [];
14080
15839
  for (const step2 of steps) {
14081
- const text = step2.text.trim();
14082
- const offset = text ? source.text.indexOf(text, cursor) : -1;
15840
+ const text2 = step2.text.trim();
15841
+ const offset = text2 ? source.text.indexOf(text2, cursor) : -1;
14083
15842
  if (offset < 0) return block;
14084
15843
  const word = narration.bookmarks.find(
14085
- (word2) => word2.charOffset >= source.charStart + offset && word2.charOffset < source.charStart + offset + text.length
15844
+ (word2) => word2.charOffset >= source.charStart + offset && word2.charOffset < source.charStart + offset + text2.length
14086
15845
  );
14087
15846
  if (!word) return block;
14088
15847
  starts.push(Math.max(0, word.time - block.startTime));
14089
- cursor = offset + text.length;
15848
+ cursor = offset + text2.length;
14090
15849
  }
14091
15850
  return { ...block, buildTiming: { sourceText: buildSourceText(steps), starts } };
14092
15851
  });
@@ -14109,10 +15868,16 @@ function listParagraphs(list, level = 0) {
14109
15868
  const ordered = Boolean(list.ordered);
14110
15869
  return list.children.flatMap((item, index) => {
14111
15870
  const ownText = item.children.filter((node) => node.type !== "list").map((node) => extractPlainText(node).trim()).filter(Boolean).join(" ");
15871
+ const checked = typeof item.checked === "boolean" ? { checked: item.checked } : {};
14112
15872
  const paragraphs = ownText ? [
14113
15873
  {
14114
15874
  text: ownText,
14115
- list: { ordered, level, ...ordered ? { number: (list.start ?? 1) + index } : {} }
15875
+ list: {
15876
+ ordered,
15877
+ level,
15878
+ ...ordered ? { number: (list.start ?? 1) + index } : {},
15879
+ ...checked
15880
+ }
14116
15881
  }
14117
15882
  ] : [];
14118
15883
  for (const child of item.children) {
@@ -14121,10 +15886,10 @@ function listParagraphs(list, level = 0) {
14121
15886
  return paragraphs;
14122
15887
  });
14123
15888
  }
14124
- function plainParagraphText({ text, list }) {
14125
- if (!list) return text;
14126
- const marker = list.ordered ? `${list.number ?? 1}.` : "\u2022";
14127
- return `${" ".repeat(list.level)}${marker} ${text}`;
15889
+ function plainParagraphText({ text: text2, list }) {
15890
+ if (!list) return text2;
15891
+ const marker = list.checked !== void 0 ? taskMarker(list.checked) : list.ordered ? `${list.number ?? 1}.` : "\u2022";
15892
+ return `${" ".repeat(list.level)}${marker} ${text2}`;
14128
15893
  }
14129
15894
  function completeBodyParagraphs(nodes) {
14130
15895
  const paragraphs = nodes.flatMap((node, index) => {
@@ -14245,7 +16010,15 @@ export {
14245
16010
  countUpSpecFor,
14246
16011
  formatCountValue,
14247
16012
  formatCountUp,
14248
- splitConceptItem,
16013
+ listBlock,
16014
+ CONCEPT_TEMPLATES,
16015
+ CONCEPT_MAX_DRAWN,
16016
+ CONCEPT_SPECS,
16017
+ conceptTemplate,
16018
+ conceptItemCount,
16019
+ conceptItemsProblem,
16020
+ conceptDowngrade,
16021
+ conceptDrawsAs,
14249
16022
  conceptBuildStepTexts,
14250
16023
  cardsBlock,
14251
16024
  processBlock,
@@ -14264,7 +16037,12 @@ export {
14264
16037
  mediaOnly,
14265
16038
  mapBlock,
14266
16039
  fullBleedQuote,
14267
- listBlock,
16040
+ statsBlock,
16041
+ pyramidBlock,
16042
+ funnelBlock,
16043
+ hubBlock,
16044
+ vennBlock,
16045
+ matrixBlock,
14268
16046
  photoGrid,
14269
16047
  definitionCard,
14270
16048
  comparisonBar,
@@ -14314,20 +16092,39 @@ export {
14314
16092
  materializeBlockLayersWithRuntime,
14315
16093
  TEMPLATE_METADATA,
14316
16094
  TEMPLATE_AUTHORING_METADATA,
16095
+ PRESENTATION_KEY,
16096
+ PRESENTATION_LAYOUTS,
16097
+ AUTHORED_PRESENTATION_LAYOUT,
16098
+ presentationStat,
16099
+ presentationLayoutFits,
16100
+ presentationLayoutProblem,
16101
+ presentationLayoutNeeds,
16102
+ presentationBeatExtras,
16103
+ PRESENTATION_LIMITS,
16104
+ parsePresentationPlan,
16105
+ serializePresentationPlan,
14317
16106
  TEMPLATE_GUIDES,
14318
16107
  guideText,
14319
16108
  templateCatalog,
14320
16109
  PRESENTATION_LAYOUT_GUIDE,
16110
+ PRESENTATION_LAYOUT_LABELS,
14321
16111
  AUTHORING_TRICKS,
14322
16112
  authoringTricks,
14323
16113
  coverBlock,
14324
16114
  expandCoverBlock,
16115
+ MARKDOWN_SHAPE_EXAMPLES,
16116
+ markdownShapeExample,
16117
+ markdownShapeGuide,
16118
+ markdownShapeSummary,
14325
16119
  expandDocBlocks,
14326
16120
  getAvailableTemplates,
14327
16121
  hasTemplate,
14328
16122
  detectPromotableBodyAnnotation,
16123
+ blockVideoLength,
16124
+ collectVideoDurationSources,
14329
16125
  getPinnedBlockMeta,
14330
16126
  markdownToDoc,
16127
+ applyVideoDurations,
14331
16128
  flattenBlocks,
14332
16129
  flattenRenderableBlocks,
14333
16130
  countBlocks,