@bendyline/squisq 2.13.13 → 2.13.15

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 (35) hide show
  1. package/dist/{Doc-BhvotZx_.d.ts → Doc-C_Sh49ot.d.ts} +59 -6
  2. package/dist/{ImageEditDoc-D8hYio6b.d.ts → ImageEditDoc-Sqw3Tw-R.d.ts} +1 -1
  3. package/dist/{chunk-OIQ2GO2O.js → chunk-2CB37BOU.js} +1 -1
  4. package/dist/{chunk-CTT4XAGO.js → chunk-4HQ7QZKB.js} +2289 -630
  5. package/dist/chunk-C7DKMO4H.js +88 -0
  6. package/dist/{chunk-C4SB3NN4.js → chunk-DDJHIZED.js} +1 -1
  7. package/dist/{chunk-TCGGJUKC.js → chunk-FYP7GNER.js} +93 -15
  8. package/dist/chunk-N3FQSUVT.js +515 -0
  9. package/dist/{chunk-SGNWO7OF.js → chunk-OFE5RZWW.js} +490 -24
  10. package/dist/{chunk-YQQWE4HG.js → chunk-OJCAKUHW.js} +192 -260
  11. package/dist/{chunk-7MS26OXD.js → chunk-UDPPWYYM.js} +6 -0
  12. package/dist/{chunk-5DFFVW75.js → chunk-Y34KYMAS.js} +1 -1
  13. package/dist/doc/index.d.ts +337 -22
  14. package/dist/doc/index.js +56 -8
  15. package/dist/fence/index.d.ts +1 -1
  16. package/dist/generate/index.d.ts +1 -1
  17. package/dist/imageEdit/index.d.ts +3 -3
  18. package/dist/index.d.ts +8 -8
  19. package/dist/index.js +127 -21
  20. package/dist/jsonForm/index.d.ts +1 -1
  21. package/dist/markdown/index.js +3 -3
  22. package/dist/{materializePageSection-CWxou4mX.d.ts → materializePageSection-Dmf-rf1b.d.ts} +121 -8
  23. package/dist/mediaEdit/index.d.ts +2 -2
  24. package/dist/narration/index.d.ts +1 -1
  25. package/dist/narration/index.js +4 -4
  26. package/dist/recommend/index.d.ts +129 -2
  27. package/dist/recommend/index.js +11 -3
  28. package/dist/schemas/index.d.ts +4 -4
  29. package/dist/{themeLibrary-BIYE6aLw.d.ts → themeLibrary-B4VLovhW.d.ts} +1 -1
  30. package/dist/timing/index.d.ts +66 -2
  31. package/dist/timing/index.js +12 -0
  32. package/dist/transform/index.d.ts +173 -16
  33. package/dist/transform/index.js +55 -16
  34. package/package.json +1 -1
  35. 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-OFE5RZWW.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,
@@ -67,7 +75,7 @@ import {
67
75
  TABLE_FED_TEMPLATES,
68
76
  isContainerTemplate,
69
77
  resolveTemplateName
70
- } from "./chunk-7MS26OXD.js";
78
+ } from "./chunk-UDPPWYYM.js";
71
79
  import {
72
80
  KNOWN_BLOCK_META_KEYS,
73
81
  coerceAnnotationValues,
@@ -629,8 +637,8 @@ var FALLBACK_COLOR_SCHEME = {
629
637
  function resolveColorScheme(context, name) {
630
638
  const schemes = context.theme.colorSchemes;
631
639
  if (name && name in schemes) return schemes[name];
632
- const keys = Object.keys(schemes);
633
- if (keys.length > 0) return schemes[keys[0]];
640
+ const keys2 = Object.keys(schemes);
641
+ if (keys2.length > 0) return schemes[keys2[0]];
634
642
  return FALLBACK_COLOR_SCHEME;
635
643
  }
636
644
  var FALLBACK_BODY = 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
@@ -722,8 +730,8 @@ function getTemplateHint(context, templateName, key, fallback) {
722
730
  // src/doc/templates/captionUtils.ts
723
731
  var TRAILING_PUNCT_RE = /[.;:!?]+$/;
724
732
  var CAPTION_BASELINE_SHIFT_EM = 0.055;
725
- function cleanCaption(text) {
726
- return text.replace(TRAILING_PUNCT_RE, "").trim();
733
+ function cleanCaption(text2) {
734
+ return text2.replace(TRAILING_PUNCT_RE, "").trim();
727
735
  }
728
736
  function createBackgroundLayer(id, fill) {
729
737
  return {
@@ -733,19 +741,19 @@ function createBackgroundLayer(id, fill) {
733
741
  position: { x: 0, y: 0, width: "100%", height: "100%" }
734
742
  };
735
743
  }
736
- function estimateTextHeight(text, fontSizePx, maxWidthPx, lineHeight) {
744
+ function estimateTextHeight(text2, fontSizePx, maxWidthPx, lineHeight) {
737
745
  const avgCharWidth = fontSizePx * 0.52;
738
746
  const charsPerLine = Math.max(8, Math.floor(maxWidthPx / avgCharWidth));
739
- const lines = Math.max(1, Math.ceil(text.length / charsPerLine));
747
+ const lines = Math.max(1, Math.ceil(text2.length / charsPerLine));
740
748
  return lines * fontSizePx * lineHeight;
741
749
  }
742
- function estimateWrappedLineCount(text, fontSize, maxWidth) {
743
- if (!text.trim()) return 1;
750
+ function estimateWrappedLineCount(text2, fontSize, maxWidth) {
751
+ if (!text2.trim()) return 1;
744
752
  const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
745
753
  if (charsPerLine <= 0) return 1;
746
754
  let lineCount = 0;
747
755
  let currentLineLength = 0;
748
- for (const word of text.split(/\s+/)) {
756
+ for (const word of text2.split(/\s+/)) {
749
757
  const testLineLength = currentLineLength ? currentLineLength + 1 + word.length : word.length;
750
758
  if (testLineLength <= charsPerLine) {
751
759
  currentLineLength = testLineLength;
@@ -761,11 +769,11 @@ function estimateWrappedLineCount(text, fontSize, maxWidth) {
761
769
  }
762
770
  return Math.max(1, lineCount + (currentLineLength ? 1 : 0));
763
771
  }
764
- function estimateProseLineCount(text, fontSize, maxWidthPx) {
765
- return text.split("\n").reduce((sum, line) => sum + estimateWrappedLineCount(line, fontSize, maxWidthPx), 0);
772
+ function estimateProseLineCount(text2, fontSize, maxWidthPx) {
773
+ return text2.split("\n").reduce((sum, line) => sum + estimateWrappedLineCount(line, fontSize, maxWidthPx), 0);
766
774
  }
767
775
  function fitProse(options) {
768
- const { text, baseFontSize, minFontSize, maxWidthPx, maxHeightPx, lineHeight } = options;
776
+ const { text: text2, baseFontSize, minFontSize, maxWidthPx, maxHeightPx, lineHeight } = options;
769
777
  const step2 = Math.max(0.5, options.step ?? 2);
770
778
  const floor = Math.min(baseFontSize, minFontSize);
771
779
  const heightAt = (lines, size) => lines * size * lineHeight;
@@ -773,7 +781,7 @@ function fitProse(options) {
773
781
  for (let size = baseFontSize; size > floor; size -= step2) candidates.push(size);
774
782
  candidates.push(floor);
775
783
  for (const size of candidates) {
776
- const lines = estimateProseLineCount(text, size, maxWidthPx);
784
+ const lines = estimateProseLineCount(text2, size, maxWidthPx);
777
785
  const heightPx = heightAt(lines, size);
778
786
  if (heightPx <= maxHeightPx) return { fontSize: size, lines, heightPx };
779
787
  }
@@ -1292,8 +1300,8 @@ function sectionHeader(input, context) {
1292
1300
  // src/doc/utils/textMetrics.ts
1293
1301
  var AVERAGE_CHAR_WIDTH_EM = 0.62;
1294
1302
  var BOLD_CHAR_WIDTH_EM = 0.66;
1295
- function estimateTextWidth(text, fontSize, charWidthEm = AVERAGE_CHAR_WIDTH_EM) {
1296
- return text.length * fontSize * charWidthEm;
1303
+ function estimateTextWidth(text2, fontSize, charWidthEm = AVERAGE_CHAR_WIDTH_EM) {
1304
+ return text2.length * fontSize * charWidthEm;
1297
1305
  }
1298
1306
  var DISPLAY_CHAR_EM = {
1299
1307
  space: 0.27,
@@ -1319,9 +1327,9 @@ function displayCharEm(ch) {
1319
1327
  if (".,:;-/'\u2019!?()".includes(ch)) return DISPLAY_CHAR_EM.punct;
1320
1328
  return DISPLAY_CHAR_EM.other;
1321
1329
  }
1322
- function estimateDisplayTextWidth(text, fontSize) {
1330
+ function estimateDisplayTextWidth(text2, fontSize) {
1323
1331
  let em = 0;
1324
- for (const ch of text.normalize("NFD").replace(/\p{M}/gu, "")) em += displayCharEm(ch);
1332
+ for (const ch of text2.normalize("NFD").replace(/\p{M}/gu, "")) em += displayCharEm(ch);
1325
1333
  return em * fontSize;
1326
1334
  }
1327
1335
 
@@ -1658,8 +1666,8 @@ function collectListItems(block) {
1658
1666
  for (const node of block.contents) {
1659
1667
  walkMarkdownTree(node, (n) => {
1660
1668
  if (n.type === "listItem") {
1661
- const text = extractPlainText(n).replace(/\s+/g, " ").trim();
1662
- if (text) out.push(text);
1669
+ const text2 = extractPlainText(n).replace(/\s+/g, " ").trim();
1670
+ if (text2) out.push(text2);
1663
1671
  return true;
1664
1672
  }
1665
1673
  return;
@@ -1675,14 +1683,14 @@ function getChildrenSummary(block) {
1675
1683
  if (!block.children || block.children.length === 0) return "";
1676
1684
  return block.children.map((c) => c.title?.trim()).filter((t) => Boolean(t)).join(", ");
1677
1685
  }
1678
- function escapeBraces(text) {
1679
- return text.replace(ESCAPE_RE, (_match, inner) => ESCAPED_OPEN + inner + ESCAPED_CLOSE);
1686
+ function escapeBraces(text2) {
1687
+ return text2.replace(ESCAPE_RE, (_match, inner) => ESCAPED_OPEN + inner + ESCAPED_CLOSE);
1680
1688
  }
1681
- function unescapeBraces(text) {
1682
- return text.replace(new RegExp(ESCAPED_OPEN, "g"), "{").replace(new RegExp(ESCAPED_CLOSE, "g"), "}");
1689
+ function unescapeBraces(text2) {
1690
+ return text2.replace(new RegExp(ESCAPED_OPEN, "g"), "{").replace(new RegExp(ESCAPED_CLOSE, "g"), "}");
1683
1691
  }
1684
- function substitute(text, resolve) {
1685
- const escaped = escapeBraces(text);
1692
+ function substitute(text2, resolve) {
1693
+ const escaped = escapeBraces(text2);
1686
1694
  const tokenized = escaped.replace(
1687
1695
  TOKEN_RE,
1688
1696
  (match, name, arg, def) => {
@@ -1725,8 +1733,8 @@ function resolveToken(name, arg, block, images, item) {
1725
1733
  return null;
1726
1734
  }
1727
1735
  }
1728
- function substituteText(text, block, images, item) {
1729
- return substitute(text, (name, arg) => resolveToken(name, arg, block, images, item));
1736
+ function substituteText(text2, block, images, item) {
1737
+ return substitute(text2, (name, arg) => resolveToken(name, arg, block, images, item));
1730
1738
  }
1731
1739
  function resolveImageSrc(src, block, images, item) {
1732
1740
  if (!src.includes("{")) return src;
@@ -1786,7 +1794,7 @@ function collectRepeatItems(source, block, images) {
1786
1794
  index: i + 1
1787
1795
  }));
1788
1796
  case "listItems":
1789
- return collectListItems(block).map((text, i) => ({ text, label: text, index: i + 1 }));
1797
+ return collectListItems(block).map((text2, i) => ({ text: text2, label: text2, index: i + 1 }));
1790
1798
  default:
1791
1799
  return [];
1792
1800
  }
@@ -1873,21 +1881,22 @@ function makeCustomTemplateFn(def) {
1873
1881
 
1874
1882
  // src/doc/utils/countUp.ts
1875
1883
  var STAT_PATTERN = /^([^\d]*?)([-−]?\d[\d,]*(?:\.\d+)?)(.*)$/s;
1876
- function parseStatNumber(text) {
1877
- const match = STAT_PATTERN.exec(text.trim());
1884
+ 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;
1885
+ function parseStatNumber(text2) {
1886
+ const match = STAT_PATTERN.exec(text2.trim());
1878
1887
  if (!match) return null;
1879
1888
  const [, prefix, numeric, suffix] = match;
1880
1889
  const normalized = numeric.replace(/,/g, "").replace("\u2212", "-");
1881
1890
  const value = Number(normalized);
1882
1891
  if (!Number.isFinite(value)) return null;
1883
1892
  const decimals = normalized.includes(".") ? normalized.split(".")[1].length : 0;
1884
- const bare = /^-?\d{4}$/.test(normalized);
1893
+ const bare = /^-?\d{4}$/.test(numeric.replace("\u2212", "-"));
1885
1894
  const looksLikeYear = bare && Math.abs(value) >= 1e3 && Math.abs(value) <= 2999 && !/^[$€£¥]/.test(prefix);
1886
- if (looksLikeYear && !/^[%KMB]/i.test(suffix.trim())) return null;
1895
+ if (looksLikeYear && !/^[%KMB]/i.test(suffix.trim()) && !MEASURE_UNIT.test(suffix)) return null;
1887
1896
  return { prefix, value, decimals, grouping: numeric.includes(","), suffix };
1888
1897
  }
1889
- function countUpSpecFor(text) {
1890
- const parsed = parseStatNumber(text);
1898
+ function countUpSpecFor(text2) {
1899
+ const parsed = parseStatNumber(text2);
1891
1900
  if (!parsed) return null;
1892
1901
  return {
1893
1902
  from: 0,
@@ -1909,13 +1918,13 @@ function formatCountValue(value, decimals, grouping) {
1909
1918
  const sign = value < 0 && Number(fixed) !== 0 ? "-" : "";
1910
1919
  return `${sign}${grouped}${fraction ? `.${fraction}` : ""}`;
1911
1920
  }
1912
- function formatCountUp(text, animation, blockTime) {
1913
- const count = animation.count ?? countUpSpecFor(text);
1914
- if (!count) return text;
1921
+ function formatCountUp(text2, animation, blockTime) {
1922
+ const count = animation.count ?? countUpSpecFor(text2);
1923
+ if (!count) return text2;
1915
1924
  const delay = animation.delay ?? 0;
1916
1925
  const duration = animation.duration ?? 1.5;
1917
1926
  const progress = duration <= 0 ? 1 : Math.min(1, Math.max(0, (blockTime - delay) / duration));
1918
- if (progress >= 1) return animation.count ? formatFinal(count) : text;
1927
+ if (progress >= 1) return animation.count ? formatFinal(count) : text2;
1919
1928
  const from = count.from ?? 0;
1920
1929
  const value = from + (count.to - from) * easeOutCubic(progress);
1921
1930
  return `${count.prefix ?? ""}${formatCountValue(value, count.decimals ?? 0, count.grouping ?? false)}${count.suffix ?? ""}`;
@@ -1924,184 +1933,571 @@ function formatFinal(count) {
1924
1933
  return `${count.prefix ?? ""}${formatCountValue(count.to, count.decimals ?? 0, count.grouping ?? false)}${count.suffix ?? ""}`;
1925
1934
  }
1926
1935
 
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) {
1936
+ // src/doc/templates/listBlock.ts
1937
+ var LIST_ITEMS_FLOW = "list-items";
1938
+ var LIST_ITEM_LINE_HEIGHT = 1.2;
1939
+ var LIST_ITEM_BASE_FONT_PX = 34;
1940
+ var LIST_ITEM_GAP_PX = 18;
1941
+ var LIST_ITEM_MIN_FONT_PX = 16;
1942
+ var LIST_BOTTOM_MARGIN_PCT = 8;
1943
+ var LIST_STRIP_CLEARANCE_PCT = 2;
1944
+ var LIST_RELAXED_TITLE_TOP_PCT = 20;
1945
+ var LIST_RELAXED_ITEMS_TOP_PCT = 34;
1946
+ var LIST_RELAXED_UNTITLED_TOP_PCT = 26;
1947
+ var LIST_CONDENSED_TOP_PCT = 8;
1948
+ var LIST_CONDENSED_TITLE_GAP_PX = 22;
1949
+ var LIST_TITLE_LINE_HEIGHT = 1.15;
1950
+ function listItemGeometry(fontSize, baseFontSize, itemCount, textWidthPx, ordered = true) {
1951
+ const markerText = ordered ? `${itemCount}.` : "\u2022";
1952
+ const markerWidthPx = Math.max(fontSize, markerText.length * fontSize * 0.5);
1953
+ const markerGapPx = fontSize * 0.35;
1954
+ const bodyIndentPx = markerWidthPx + markerGapPx;
1950
1955
  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)
1956
+ markerWidthPx,
1957
+ bodyIndentPx,
1958
+ bodyWidthPx: Math.max(fontSize, textWidthPx - bodyIndentPx),
1959
+ gapPx: LIST_ITEM_GAP_PX * (fontSize / baseFontSize)
1954
1960
  };
1955
1961
  }
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 };
1962
+ function measureListHeightPx(items, fontSize, geometry) {
1963
+ let heightPx = 0;
1964
+ for (const itemText2 of items) {
1965
+ const lineCount = estimateProseLineCount(itemText2, fontSize, geometry.bodyWidthPx);
1966
+ heightPx += lineCount * fontSize * LIST_ITEM_LINE_HEIGHT + geometry.gapPx;
1972
1967
  }
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;
1968
+ return heightPx;
1977
1969
  }
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({
1970
+ function listBlock(input, context) {
1971
+ const { title } = input;
1972
+ const items = Array.isArray(input.items) ? input.items : [];
1973
+ if (items.length === 0 && !context.block?.summaryMode) return [];
1974
+ const build = input.build ?? context.block?.build;
1975
+ if (build && build.mode !== "off") {
1976
+ const rich = extractRichListItems(context.block?.contents);
1977
+ const matches = rich.length === items.length && rich.every((item, i) => item.text === items[i]);
1978
+ const contents = matches ? context.block?.contents ?? [] : [
1979
+ {
1980
+ type: "list",
1981
+ ordered: input.ordered !== false,
1982
+ children: items.map((text2) => ({
1983
+ type: "listItem",
1984
+ children: [{ type: "paragraph", children: [{ type: "text", value: text2 }] }]
1985
+ }))
1986
+ }
1987
+ ];
1988
+ const layers2 = contentBlock(
1989
+ { ...input, template: "content", title: input.title ?? "", build },
1990
+ {
1991
+ ...context,
1992
+ block: {
1993
+ id: input.id,
1994
+ duration: input.duration,
1995
+ startTime: 0,
1996
+ audioSegment: input.audioSegment,
1997
+ ...context.block,
1998
+ summaryMode: void 0,
1999
+ contents,
2000
+ build
2001
+ }
2002
+ }
2003
+ );
2004
+ const accent = accentForViewport(input.accentImage, context.viewport);
2005
+ if (accent) {
2006
+ const layout = getAccentLayout(accent.position);
2007
+ layers2.splice(
2008
+ 1,
2009
+ 0,
2010
+ ...createAccentLayers(
2011
+ accent,
2012
+ input.id,
2013
+ themedImageTreatment(context, input.imageTreatment)
2014
+ )
2015
+ );
2016
+ for (const layer of layers2) {
2017
+ if (layer.type !== "text" || layer.id !== "title" && layer.id !== "body") continue;
2018
+ layer.position.x = `${parseFloat(layout.textCenterX) - parseFloat(layout.textWidth) / 2}%`;
2019
+ layer.position.width = layout.textWidth;
2020
+ layer.position.y = adjustY(String(layer.position.y), layout);
2021
+ if (layer.id === "body" && accent.position === "bottom-strip")
2022
+ layer.position.height = `${72 - ACCENT_STRIP_SIZE}%`;
2023
+ }
2024
+ }
2025
+ return layers2;
2026
+ }
2027
+ const accentImage = accentForViewport(input.accentImage, context.viewport);
2028
+ const ordered = input.ordered !== false;
2029
+ const richItems = extractRichListItems(context.block?.contents);
2030
+ const { theme } = context;
2031
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
2032
+ const titleFontSize = themedFontSize(44, context, true);
2033
+ const baseItemFontSize = themedFontSize(LIST_ITEM_BASE_FONT_PX, context, false);
2034
+ const minItemFontSize = Math.min(
2035
+ baseItemFontSize,
2036
+ themedFontSize(LIST_ITEM_MIN_FONT_PX, context, false)
2037
+ );
2038
+ const layers = [
2039
+ // Background — gradient
2040
+ {
2000
2041
  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
2042
+ id: "bg",
2043
+ content: {
2044
+ shape: "rect",
2045
+ fill: `linear-gradient(155deg, ${theme.colors.backgroundLight} 0%, ${theme.colors.background} 100%)`
2007
2046
  },
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 }
2047
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2046
2048
  }
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;
2049
+ ];
2050
+ if (accentImage) {
2071
2051
  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
- }
2052
+ ...createAccentLayers(
2053
+ accentImage,
2054
+ input.id,
2055
+ themedImageTreatment(context, input.imageTreatment)
2085
2056
  )
2086
2057
  );
2087
2058
  }
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)
2097
- return dated.map((item) => [item.when, item.label, item.detail].filter(Boolean).join(" "));
2098
- return shown.map(
2099
- (item, index) => [
2100
- template === "cards" || template === "layers" ? "" : String(index + 1),
2101
- item.label,
2102
- item.detail
2103
- ].filter(Boolean).join(" ")
2104
- );
2059
+ const centerX = parseFloat(accentLayout.textCenterX);
2060
+ const widthPct = parseFloat(accentLayout.textWidth);
2061
+ const leftPct = Number.isFinite(centerX) && Number.isFinite(widthPct) ? centerX - widthPct / 2 : 8;
2062
+ const leftX = `${leftPct}%`;
2063
+ const textWidthPx = (Number.isFinite(widthPct) ? widthPct / 100 : 0.85) * context.viewport.width;
2064
+ const bottomLimitPct = (accentImage?.position === "bottom-strip" ? 100 - ACCENT_STRIP_SIZE - LIST_STRIP_CLEARANCE_PCT : 100 - LIST_BOTTOM_MARGIN_PCT) - accentLayout.textYAdjust;
2065
+ const baseGeometry = listItemGeometry(
2066
+ baseItemFontSize,
2067
+ baseItemFontSize,
2068
+ items.length,
2069
+ textWidthPx,
2070
+ ordered
2071
+ );
2072
+ const relaxedStartY = title ? LIST_RELAXED_ITEMS_TOP_PCT : LIST_RELAXED_UNTITLED_TOP_PCT;
2073
+ const relaxedBottomPct = relaxedStartY + measureListHeightPx(items, baseItemFontSize, baseGeometry) / context.viewport.height * 100;
2074
+ const condensed = relaxedBottomPct > bottomLimitPct;
2075
+ const condensedTopPct = LIST_CONDENSED_TOP_PCT - accentLayout.textYAdjust;
2076
+ const titleTopPct = condensed ? condensedTopPct : LIST_RELAXED_TITLE_TOP_PCT;
2077
+ let startY = relaxedStartY;
2078
+ if (condensed) {
2079
+ const titleHeightPx = title ? estimateWrappedLineCount(title, titleFontSize, textWidthPx) * titleFontSize * LIST_TITLE_LINE_HEIGHT + LIST_CONDENSED_TITLE_GAP_PX : 0;
2080
+ startY = condensedTopPct + titleHeightPx / context.viewport.height * 100;
2081
+ }
2082
+ if (title) {
2083
+ layers.push({
2084
+ type: "text",
2085
+ id: "list-title",
2086
+ content: {
2087
+ text: title,
2088
+ style: {
2089
+ fontSize: titleFontSize,
2090
+ fontFamily: getThemeFont(context, "title"),
2091
+ fontWeight: "bold",
2092
+ color: theme.colors.text,
2093
+ textAlign: "left",
2094
+ shadow: shouldUseShadow(context)
2095
+ }
2096
+ },
2097
+ position: {
2098
+ x: leftX,
2099
+ y: adjustY(`${titleTopPct}%`, accentLayout),
2100
+ width: accentLayout.textWidth
2101
+ },
2102
+ animation: motionEntrance(context, { type: "fadeIn", duration: 1 })
2103
+ });
2104
+ }
2105
+ const availablePx = Math.max(0, (bottomLimitPct - startY) / 100 * context.viewport.height);
2106
+ let itemFontSize = baseItemFontSize;
2107
+ let geometry = baseGeometry;
2108
+ while (itemFontSize > minItemFontSize && measureListHeightPx(items, itemFontSize, geometry) > availablePx) {
2109
+ itemFontSize -= 1;
2110
+ geometry = listItemGeometry(itemFontSize, baseItemFontSize, items.length, textWidthPx, ordered);
2111
+ }
2112
+ const { markerWidthPx, bodyIndentPx, bodyWidthPx, gapPx } = geometry;
2113
+ const bodyLeftX = `${leftPct + bodyIndentPx / context.viewport.width * 100}%`;
2114
+ let itemY = startY;
2115
+ for (let i = 0; i < items.length; i++) {
2116
+ const itemText2 = items[i];
2117
+ const itemHtml = richItems[i]?.text === itemText2 ? richItems[i]?.html : void 0;
2118
+ const lineCount = estimateProseLineCount(itemText2, itemFontSize, bodyWidthPx);
2119
+ const animation = motionEntrance(context, {
2120
+ type: "fadeIn",
2121
+ duration: 0.8,
2122
+ delay: 0.3 + 0.3 * i
2123
+ });
2124
+ layers.push({
2125
+ type: "text",
2126
+ id: `item-${i}-marker`,
2127
+ content: {
2128
+ // Bullets stay bullets; only ordered (or legacy, unflagged) lists count.
2129
+ text: ordered ? `${i + 1}.` : "\u2022",
2130
+ flow: { id: LIST_ITEMS_FLOW, role: "marker" },
2131
+ style: {
2132
+ fontSize: itemFontSize,
2133
+ fontFamily: getThemeFont(context, "body"),
2134
+ color: theme.colors.text,
2135
+ textAlign: "right",
2136
+ lineHeight: LIST_ITEM_LINE_HEIGHT,
2137
+ shadow: shouldUseShadow(context)
2138
+ }
2139
+ },
2140
+ position: {
2141
+ x: leftX,
2142
+ y: adjustY(`${itemY}%`, accentLayout),
2143
+ width: markerWidthPx
2144
+ },
2145
+ animation
2146
+ });
2147
+ layers.push({
2148
+ type: "text",
2149
+ id: `item-${i}`,
2150
+ content: {
2151
+ text: itemText2,
2152
+ ...itemHtml ? { html: itemHtml } : {},
2153
+ paragraphs: [
2154
+ {
2155
+ text: itemText2,
2156
+ list: { ordered, level: 0, ...ordered ? { number: i + 1 } : {} }
2157
+ }
2158
+ ],
2159
+ flow: { id: LIST_ITEMS_FLOW },
2160
+ style: {
2161
+ fontSize: itemFontSize,
2162
+ fontFamily: getThemeFont(context, "body"),
2163
+ color: theme.colors.text,
2164
+ textAlign: "left",
2165
+ lineHeight: LIST_ITEM_LINE_HEIGHT,
2166
+ shadow: shouldUseShadow(context)
2167
+ }
2168
+ },
2169
+ position: {
2170
+ x: bodyLeftX,
2171
+ y: adjustY(`${itemY}%`, accentLayout),
2172
+ width: bodyWidthPx,
2173
+ // Rich (HTML) items render in a hidden-overflow box the renderer
2174
+ // sizes from the plain text; size it here from the same line count
2175
+ // that reserved the vertical space so nested sub-items are not cut off.
2176
+ height: lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT + itemFontSize * 0.5
2177
+ },
2178
+ animation
2179
+ });
2180
+ const itemHeightPx = lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT;
2181
+ itemY += (itemHeightPx + gapPx) / context.viewport.height * 100;
2182
+ }
2183
+ return layers;
2184
+ }
2185
+
2186
+ // src/doc/templates/conceptSpecs.ts
2187
+ var CONCEPT_TEMPLATES = [
2188
+ "cards",
2189
+ "process",
2190
+ "cycle",
2191
+ "layers",
2192
+ "stats",
2193
+ "pyramid",
2194
+ "funnel",
2195
+ "hub",
2196
+ "venn",
2197
+ "matrix"
2198
+ ];
2199
+ var CONCEPT_MAX_DRAWN = 6;
2200
+ var CYCLE_STAGE_CHARACTERS = 170;
2201
+ var CONCEPT_SPECS = {
2202
+ cards: {
2203
+ items: [2, 4],
2204
+ itemForm: "label-detail",
2205
+ inputs: [{ key: "versus", guide: "versus: true for exactly two set against each other" }]
2206
+ },
2207
+ process: { items: [2, 4], itemForm: "label-detail", order: "in order" },
2208
+ cycle: { items: [3, 5], itemForm: "label-detail" },
2209
+ layers: { items: [3, 5], itemForm: "label-detail", order: "innermost first" },
2210
+ stats: { items: [2, 4], itemForm: "figure-caption" },
2211
+ pyramid: { items: [3, 5], itemForm: "label-detail", order: "from the top down" },
2212
+ funnel: { items: [3, 5], itemForm: "label-detail", order: "widest first" },
2213
+ hub: {
2214
+ items: [3, 6],
2215
+ itemForm: "label-detail",
2216
+ inputs: [{ key: "center", guide: "center \u2264 3 words" }]
2217
+ },
2218
+ // Two sets and the item that says what they share.
2219
+ venn: { items: [3, 3], itemForm: "label-detail" },
2220
+ matrix: {
2221
+ items: [4, 4],
2222
+ itemForm: "label-detail",
2223
+ order: "in reading order (top left, top right, bottom left, bottom right)",
2224
+ inputs: [
2225
+ { key: "xAxis", guide: "xAxis" },
2226
+ { key: "yAxis", guide: "yAxis" }
2227
+ ]
2228
+ }
2229
+ };
2230
+ function conceptTemplate(template) {
2231
+ return CONCEPT_TEMPLATES.find((name) => name === template);
2232
+ }
2233
+ function range(template, options) {
2234
+ const [min, most] = CONCEPT_SPECS[template].items;
2235
+ return [min, Math.max(min, Math.min(most, options.max ?? most))];
2236
+ }
2237
+ function conceptItemCount(template, options = {}) {
2238
+ const [min, max] = range(template, options);
2239
+ return min === max ? `exactly ${min}` : `${min}\u2013${max}`;
2240
+ }
2241
+ function itemTexts(items) {
2242
+ return (Array.isArray(items) ? items : []).map(
2243
+ (item) => String(item ?? "").replace(/\s+/gu, " ").trim()
2244
+ ).filter(Boolean);
2245
+ }
2246
+ var quoted = (text2) => `"${text2.length > 48 ? `${text2.slice(0, 47)}\u2026` : text2}"`;
2247
+ function conceptItemsProblem(template, items, options = {}) {
2248
+ const texts = itemTexts(items);
2249
+ const [min, max] = range(template, options);
2250
+ if (texts.length < min || texts.length > max)
2251
+ return `${template} takes ${conceptItemCount(template, options)} items and this has ${texts.length}`;
2252
+ if (template === "stats") {
2253
+ const bare = texts.find((text2) => !splitStatItem(text2).figure);
2254
+ if (bare !== void 0)
2255
+ return `every stats item is "figure: what it measures" and ${quoted(bare)} states no figure`;
2256
+ }
2257
+ if (template === "venn" && vennOverlapIndex(texts.map(splitConceptItem)) < 0)
2258
+ return 'venn needs one item labelled "Both:" that says what the two share';
2259
+ return void 0;
2260
+ }
2261
+ function cycleDrawsAsProcess(items) {
2262
+ return items.length < 3 || items.some((item) => item.detail.length > CYCLE_STAGE_CHARACTERS);
2263
+ }
2264
+ function conceptDowngrade(template, items) {
2265
+ const texts = itemTexts(items);
2266
+ if (texts.length > CONCEPT_MAX_DRAWN)
2267
+ return {
2268
+ drawsAs: "list",
2269
+ reason: `a drawing holds ${CONCEPT_MAX_DRAWN} items and this has ${texts.length}`
2270
+ };
2271
+ const parsed = texts.map(splitConceptItem);
2272
+ const n = parsed.length;
2273
+ switch (template) {
2274
+ case "cycle":
2275
+ if (n < 3)
2276
+ return {
2277
+ drawsAs: "process",
2278
+ reason: `a ring needs at least three stages and this has ${n}`
2279
+ };
2280
+ return cycleDrawsAsProcess(parsed) ? { drawsAs: "process", reason: "a stage runs too long to sit beside a ring" } : void 0;
2281
+ case "layers":
2282
+ case "pyramid":
2283
+ case "funnel":
2284
+ case "hub":
2285
+ return n < 2 ? { drawsAs: "cards", reason: `it needs at least two items and this has ${n}` } : void 0;
2286
+ case "stats":
2287
+ return texts.some((text2) => splitStatItem(text2).figure) ? void 0 : { drawsAs: "cards", reason: "no item states a figure" };
2288
+ case "venn": {
2289
+ const sets = n - (vennOverlapIndex(parsed) >= 0 ? 1 : 0);
2290
+ return sets === 2 ? void 0 : {
2291
+ drawsAs: "cards",
2292
+ reason: `a Venn diagram draws two sets (and one item labelled "Both:") and this has ${sets}`
2293
+ };
2294
+ }
2295
+ case "matrix":
2296
+ return n === 4 ? void 0 : { drawsAs: "cards", reason: `a matrix places exactly four items and this has ${n}` };
2297
+ default:
2298
+ return void 0;
2299
+ }
2300
+ }
2301
+ function conceptDrawsAs(template, items) {
2302
+ return conceptDowngrade(template, items)?.drawsAs ?? template;
2303
+ }
2304
+
2305
+ // src/doc/templates/conceptBlocks.ts
2306
+ var MAX_ITEMS = CONCEPT_MAX_DRAWN;
2307
+ var HEADLINE_BASE = 60;
2308
+ var HEADLINE_MIN = 40;
2309
+ var HEADLINE_LINE_HEIGHT = 1.15;
2310
+ var LABEL_BASE = 44;
2311
+ var DETAIL_BASE = 33;
2312
+ var PLAIN_BASE = 38;
2313
+ var LABEL_LINE_HEIGHT = 1.2;
2314
+ var DETAIL_LINE_HEIGHT = 1.4;
2315
+ var MIN_SCALE = 0.62;
2316
+ var MAX_SCALE = 1.18;
2317
+ var GROW_SHARE = 0.9;
2318
+ function ramp(context, scale) {
2319
+ return {
2320
+ label: Math.round(themedFontSize(LABEL_BASE, context, false) * scale),
2321
+ detail: Math.round(themedFontSize(DETAIL_BASE, context, false) * scale),
2322
+ plain: Math.round(themedFontSize(PLAIN_BASE, context, false) * scale)
2323
+ };
2324
+ }
2325
+ function measureItem(item, sizes, width) {
2326
+ const labelH = item.label ? estimateProseLineCount(item.label, sizes.label, width) * sizes.label * LABEL_LINE_HEIGHT : 0;
2327
+ const size = item.label ? sizes.detail : sizes.plain;
2328
+ const detailH = item.detail ? estimateProseLineCount(item.detail, size, width) * size * DETAIL_LINE_HEIGHT : 0;
2329
+ const gap = item.label && item.detail ? sizes.label * 0.35 : 0;
2330
+ const widest = (text2, size2, em) => Math.max(0, ...text2.split(/\s+/u).map((word) => word.length)) * size2 * em;
2331
+ const overflows = widest(item.label, sizes.label, 0.62) > width || widest(item.detail, size, 0.56) > width;
2332
+ return { labelH, gap, detailH, height: overflows ? Infinity : labelH + gap + detailH };
2333
+ }
2334
+ var SCALES = [MAX_SCALE, 1, 0.82, MIN_SCALE];
2335
+ function fitRamp(items, context, width, height) {
2336
+ for (const scale of SCALES) {
2337
+ const sizes = ramp(context, scale);
2338
+ const room = height(scale) * (scale > 1 ? GROW_SHARE : 1);
2339
+ if (items.every((item) => measureItem(item, sizes, width(scale)).height <= room))
2340
+ return { sizes, scale };
2341
+ }
2342
+ return { sizes: ramp(context, MIN_SCALE), scale: MIN_SCALE };
2343
+ }
2344
+ function accentColor(input, context) {
2345
+ return input.colorScheme ? resolveColorScheme(context, input.colorScheme).accent : context.theme.colors.highlight || context.theme.colors.primary;
2346
+ }
2347
+ function frame(input, context) {
2348
+ const { theme, viewport } = context;
2349
+ const { width: W, height: H } = viewport;
2350
+ const portrait = H > W;
2351
+ const accent = accentColor(input, context);
2352
+ const margin = Math.round(W * (portrait ? 0.08 : 0.07));
2353
+ const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
2354
+ const title = (input.title ?? "").trim();
2355
+ let top = H * 0.1;
2356
+ if (title) {
2357
+ const headlineTop = H * (portrait ? 0.08 : 0.11);
2358
+ const base = themedFontSize(HEADLINE_BASE, context, true);
2359
+ const fit = fitProse({
2360
+ text: title,
2361
+ baseFontSize: base,
2362
+ minFontSize: Math.min(base, themedFontSize(HEADLINE_MIN, context, true)),
2363
+ maxWidthPx: W - margin * 2,
2364
+ maxHeightPx: base * HEADLINE_LINE_HEIGHT * 2,
2365
+ lineHeight: HEADLINE_LINE_HEIGHT
2366
+ });
2367
+ const ruleH = Math.max(4, Math.round(H * 6e-3));
2368
+ layers.push({
2369
+ type: "shape",
2370
+ id: "accent-rule",
2371
+ position: {
2372
+ x: margin,
2373
+ y: headlineTop - ruleH - fit.fontSize * 0.55,
2374
+ width: W * 0.045,
2375
+ height: ruleH
2376
+ },
2377
+ content: { shape: "rect", fill: accent, borderRadius: ruleH / 2 },
2378
+ animation: entrance(context, "rule", 0)
2379
+ });
2380
+ layers.push({
2381
+ ...textLayer("title", title, box(margin, headlineTop, W - margin * 2, fit.heightPx), {
2382
+ fontSize: fit.fontSize,
2383
+ fontFamily: getThemeFont(context, "title"),
2384
+ fontWeight: "bold",
2385
+ color: theme.colors.text,
2386
+ lineHeight: HEADLINE_LINE_HEIGHT,
2387
+ shadow: shouldUseShadow(context),
2388
+ ...fit.maxLines ? { maxLines: fit.maxLines } : {}
2389
+ }),
2390
+ animation: entrance(context, "text", 0.1)
2391
+ });
2392
+ top = headlineTop + fit.heightPx + H * (portrait ? 0.05 : 0.07);
2393
+ }
2394
+ return {
2395
+ layers,
2396
+ accent,
2397
+ left: margin,
2398
+ right: W - margin,
2399
+ top,
2400
+ bottom: H * (portrait ? 0.92 : 0.9),
2401
+ portrait
2402
+ };
2403
+ }
2404
+ function box(left, top, width, height) {
2405
+ return { x: left + width / 2, y: top + height / 2, width, height, anchor: "center" };
2406
+ }
2407
+ function textLayer(id, text2, position, style) {
2408
+ return {
2409
+ type: "text",
2410
+ id,
2411
+ position,
2412
+ content: {
2413
+ text: text2,
2414
+ style: { textAlign: "left", verticalAlign: "top", shrinkToFit: true, ...style }
2415
+ }
2416
+ };
2417
+ }
2418
+ function itemText(id, item, measure, sizes, left, top, width, align, context) {
2419
+ const { theme } = context;
2420
+ const slack = (size) => size * 0.4;
2421
+ const layers = [];
2422
+ if (item.label)
2423
+ layers.push(
2424
+ textLayer(
2425
+ `${id}-label`,
2426
+ item.label,
2427
+ box(left, top, width, measure.labelH + slack(sizes.label)),
2428
+ {
2429
+ fontSize: sizes.label,
2430
+ fontFamily: getThemeFont(context, "title"),
2431
+ fontWeight: "bold",
2432
+ color: theme.colors.text,
2433
+ lineHeight: LABEL_LINE_HEIGHT,
2434
+ textAlign: align
2435
+ }
2436
+ )
2437
+ );
2438
+ if (item.detail) {
2439
+ const size = item.label ? sizes.detail : sizes.plain;
2440
+ layers.push(
2441
+ textLayer(
2442
+ `${id}-detail`,
2443
+ item.detail,
2444
+ box(left, top + measure.labelH + measure.gap, width, measure.detailH + slack(size)),
2445
+ {
2446
+ fontSize: size,
2447
+ fontFamily: getThemeFont(context, "body"),
2448
+ // Details read as secondary by weight and a little transparency, not a
2449
+ // muted hue that loses contrast on dark themes.
2450
+ color: item.label ? withAlpha(theme.colors.text, 0.8) : theme.colors.text,
2451
+ lineHeight: DETAIL_LINE_HEIGHT,
2452
+ textAlign: align
2453
+ }
2454
+ )
2455
+ );
2456
+ }
2457
+ return layers;
2458
+ }
2459
+ function itemTexts2(input) {
2460
+ return (Array.isArray(input.items) ? input.items : []).map((item) => String(item ?? "").trim()).filter(Boolean).slice(0, MAX_ITEMS);
2461
+ }
2462
+ function itemsOf(input) {
2463
+ return itemTexts2(input).map(splitConceptItem);
2464
+ }
2465
+ function withoutIdeas(area, context) {
2466
+ return context.block?.contents?.length ? [] : area.layers;
2467
+ }
2468
+ function asListWhenCrowded(input, context) {
2469
+ const items = (Array.isArray(input.items) ? input.items : []).map((item) => String(item ?? "").trim()).filter(Boolean);
2470
+ if (items.length <= MAX_ITEMS) return void 0;
2471
+ return listBlock(
2472
+ {
2473
+ ...input,
2474
+ template: "list",
2475
+ items,
2476
+ ordered: input.template === "process"
2477
+ },
2478
+ context
2479
+ );
2480
+ }
2481
+ function statItemsOf(input) {
2482
+ return itemTexts2(input).map(splitStatItem);
2483
+ }
2484
+ function conceptBuildStepTexts(template, items) {
2485
+ const shown = itemsOf({ items: [...items] });
2486
+ const stats = template === "stats" ? statItemsOf({ items: [...items] }) : [];
2487
+ if (stats.some((stat) => stat.figure))
2488
+ return stats.map((stat) => {
2489
+ const { number, unit } = splitFigure(stat.figure);
2490
+ return [number, unit, stat.caption].filter(Boolean).join(" ");
2491
+ });
2492
+ if (template !== "process" && template !== "cycle")
2493
+ return shown.map((item) => [item.label, item.detail].filter(Boolean).join(" "));
2494
+ const asProcess = template === "process" || cycleDrawsAsProcess(shown);
2495
+ const dated = asProcess ? timelineItems(shown) : void 0;
2496
+ if (dated)
2497
+ return dated.map((item) => [item.when, item.label, item.detail].filter(Boolean).join(" "));
2498
+ return shown.map(
2499
+ (item, index) => [String(index + 1), item.label, item.detail].filter(Boolean).join(" ")
2500
+ );
2105
2501
  }
2106
2502
  var FIRST_TURN = 0.6;
2107
2503
  var TURN_STAGGER = 0.45;
@@ -2149,18 +2545,18 @@ function words(layers, at) {
2149
2545
  var LEAD_BASE = 44;
2150
2546
  var LEAD_MIN = 28;
2151
2547
  var LEAD_LINE_HEIGHT = 1.4;
2152
- function lead(area, text, context) {
2548
+ function lead(area, text2, context) {
2153
2549
  const { theme, viewport } = context;
2154
2550
  const barW = Math.max(5, Math.round(viewport.width * 4e-3));
2155
2551
  const indent = barW + Math.round(viewport.width * 0.018);
2156
2552
  const width = (area.right - area.left) * (area.portrait ? 1 : 0.8) - indent;
2157
2553
  const base = themedFontSize(
2158
- text.length <= 90 ? LEAD_BASE * 1.3 : text.length <= 160 ? LEAD_BASE * 1.15 : LEAD_BASE,
2554
+ text2.length <= 90 ? LEAD_BASE * 1.3 : text2.length <= 160 ? LEAD_BASE * 1.15 : LEAD_BASE,
2159
2555
  context,
2160
2556
  false
2161
2557
  );
2162
2558
  const fit = fitProse({
2163
- text,
2559
+ text: text2,
2164
2560
  baseFontSize: base,
2165
2561
  minFontSize: Math.min(base, themedFontSize(LEAD_MIN, context, false)),
2166
2562
  maxWidthPx: width,
@@ -2190,7 +2586,7 @@ function lead(area, text, context) {
2190
2586
  [
2191
2587
  textLayer(
2192
2588
  "lead",
2193
- text,
2589
+ text2,
2194
2590
  box(area.left + indent, top, width, fit.heightPx + fit.fontSize * 0.4),
2195
2591
  {
2196
2592
  fontSize: fit.fontSize,
@@ -2212,13 +2608,13 @@ function lead(area, text, context) {
2212
2608
  }
2213
2609
  var STATEMENT_BASE = 84;
2214
2610
  var STATEMENT_MIN = 48;
2215
- function statement(area, text, context) {
2611
+ function statement(area, text2, context) {
2216
2612
  const { theme, viewport } = context;
2217
2613
  const W = area.right - area.left;
2218
2614
  const H = area.bottom - area.top;
2219
2615
  const base = themedFontSize(STATEMENT_BASE, context, true);
2220
2616
  const fit = fitProse({
2221
- text,
2617
+ text: text2,
2222
2618
  baseFontSize: base,
2223
2619
  minFontSize: Math.min(base, themedFontSize(STATEMENT_MIN, context, true)),
2224
2620
  maxWidthPx: W * (area.portrait ? 1 : 0.86),
@@ -2249,7 +2645,7 @@ function statement(area, text, context) {
2249
2645
  [
2250
2646
  textLayer(
2251
2647
  "statement",
2252
- text,
2648
+ text2,
2253
2649
  box(area.left, top, W * (area.portrait ? 1 : 0.86), fit.heightPx + fit.fontSize * 0.3),
2254
2650
  {
2255
2651
  fontSize: fit.fontSize,
@@ -2271,10 +2667,12 @@ function statement(area, text, context) {
2271
2667
  ];
2272
2668
  }
2273
2669
  function cardsBlock(input, context) {
2670
+ const crowded = asListWhenCrowded(input, context);
2671
+ if (crowded) return crowded;
2274
2672
  const items = itemsOf(input);
2275
2673
  const area = frame(input, context);
2276
2674
  const { theme, viewport } = context;
2277
- if (!items.length) return area.layers;
2675
+ if (!items.length) return withoutIdeas(area, context);
2278
2676
  if (items.length === 1 && !items[0].label)
2279
2677
  return (input.title?.trim() ? lead : statement)(area, items[0].detail, context);
2280
2678
  const contrast = String(input.versus) === "true";
@@ -2506,10 +2904,12 @@ function connector(id, d, color, width, arrow = true) {
2506
2904
  };
2507
2905
  }
2508
2906
  function processBlock(input, context) {
2907
+ const crowded = asListWhenCrowded(input, context);
2908
+ if (crowded) return crowded;
2509
2909
  const items = itemsOf(input);
2510
2910
  const area = frame(input, context);
2511
2911
  const { viewport } = context;
2512
- if (!items.length) return area.layers;
2912
+ if (!items.length) return withoutIdeas(area, context);
2513
2913
  const dated = timelineItems(items);
2514
2914
  if (dated) return timeline(area, dated, context);
2515
2915
  const W = area.right - area.left;
@@ -2638,30 +3038,6 @@ function processBlock(input, context) {
2638
3038
  });
2639
3039
  return layers;
2640
3040
  }
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
3041
  function timeline(area, items, context) {
2666
3042
  const { viewport } = context;
2667
3043
  const W = area.right - area.left;
@@ -2824,10 +3200,12 @@ function timeline(area, items, context) {
2824
3200
  return layers;
2825
3201
  }
2826
3202
  function cycleBlock(input, context) {
3203
+ const crowded = asListWhenCrowded(input, context);
3204
+ if (crowded) return crowded;
2827
3205
  const items = itemsOf(input);
2828
3206
  const area = frame(input, context);
2829
3207
  const { viewport } = context;
2830
- if (items.length < 2) return processBlock({ ...input, template: "process" }, context);
3208
+ if (cycleDrawsAsProcess(items)) return processBlock({ ...input, template: "process" }, context);
2831
3209
  const H = area.bottom - area.top;
2832
3210
  const n = items.length;
2833
3211
  const cx = (area.left + area.right) / 2;
@@ -2912,8 +3290,11 @@ function cycleBlock(input, context) {
2912
3290
  return layers;
2913
3291
  }
2914
3292
  function layersBlock(input, context) {
3293
+ const crowded = asListWhenCrowded(input, context);
3294
+ if (crowded) return crowded;
2915
3295
  const items = itemsOf(input);
2916
- if (items.length < 2) return cardsBlock({ ...input, template: "cards" }, context);
3296
+ if (conceptDrawsAs("layers", input.items) === "cards")
3297
+ return cardsBlock({ ...input, template: "cards" }, context);
2917
3298
  const area = frame(input, context);
2918
3299
  const { theme, viewport } = context;
2919
3300
  const W = area.right - area.left;
@@ -4241,10 +4622,10 @@ function estimateLineEm(line, mono) {
4241
4622
  }
4242
4623
  return em;
4243
4624
  }
4244
- function simulateRendererLines(text, fontSize, boxWidthPx) {
4625
+ function simulateRendererLines(text2, fontSize, boxWidthPx) {
4245
4626
  const charsPerLine = Math.floor(boxWidthPx / (fontSize * RENDERER_CHAR_EM));
4246
4627
  const lines = [];
4247
- for (const segment of text.split("\n")) {
4628
+ for (const segment of text2.split("\n")) {
4248
4629
  if (!segment.trim() || charsPerLine <= 0) {
4249
4630
  lines.push(segment);
4250
4631
  continue;
@@ -4268,14 +4649,14 @@ function simulateRendererLines(text, fontSize, boxWidthPx) {
4268
4649
  }
4269
4650
  return lines;
4270
4651
  }
4271
- function fitWrapWidth(text, fontSize, columnWidthPx, fontFamily) {
4652
+ function fitWrapWidth(text2, fontSize, columnWidthPx, fontFamily) {
4272
4653
  const mono = isMonoFamily(fontFamily);
4273
- if (!mono && !WIDE_GLYPH_RE.test(text)) return columnWidthPx;
4654
+ if (!mono && !WIDE_GLYPH_RE.test(text2)) return columnWidthPx;
4274
4655
  const floor = columnWidthPx * 0.5;
4275
4656
  const step2 = columnWidthPx * 0.02;
4276
4657
  let width = columnWidthPx;
4277
4658
  while (width > floor) {
4278
- const lines = simulateRendererLines(text, fontSize, width);
4659
+ const lines = simulateRendererLines(text2, fontSize, width);
4279
4660
  const widest = Math.max(...lines.map((line) => estimateLineEm(line, mono))) * fontSize;
4280
4661
  if (widest <= columnWidthPx) return width;
4281
4662
  width -= step2;
@@ -4542,24 +4923,24 @@ var LINE_HEIGHT = 1.2;
4542
4923
  var FULL_SIZE_CHARS = 40;
4543
4924
  var MIN_SIZE_CHARS = 160;
4544
4925
  var MIN_LENGTH_FACTOR = 0.6;
4545
- function lengthFactor(text) {
4546
- const length = text.trim().length;
4926
+ function lengthFactor(text2) {
4927
+ const length = text2.trim().length;
4547
4928
  if (length <= FULL_SIZE_CHARS) return 1;
4548
4929
  const t = Math.min(1, (length - FULL_SIZE_CHARS) / (MIN_SIZE_CHARS - FULL_SIZE_CHARS));
4549
4930
  return 1 - t * (1 - MIN_LENGTH_FACTOR);
4550
4931
  }
4551
4932
  function fullBleedQuote(input, context) {
4552
- const { text, colorScheme = "blue" } = input;
4933
+ const { text: text2, colorScheme = "blue" } = input;
4553
4934
  const { viewport } = context;
4554
4935
  const colors = resolveColorScheme(context, colorScheme);
4555
4936
  const H = viewport.height;
4556
4937
  const W = viewport.width;
4557
4938
  const pct2 = (px) => `${Number((px / H * 100).toFixed(3))}%`;
4558
4939
  const impactFontSize = themedFontSize(120, context, true);
4559
- const baseFontSize = Math.round(impactFontSize * lengthFactor(text));
4940
+ const baseFontSize = Math.round(impactFontSize * lengthFactor(text2));
4560
4941
  const bandHeightPx = (BAND_BOTTOM3 - BAND_TOP3) * H;
4561
4942
  const fit = fitProse({
4562
- text,
4943
+ text: text2,
4563
4944
  baseFontSize,
4564
4945
  minFontSize: Math.max(28, impactFontSize / 3),
4565
4946
  maxWidthPx: COLUMN_WIDTH * W,
@@ -4588,7 +4969,7 @@ function fullBleedQuote(input, context) {
4588
4969
  type: "text",
4589
4970
  id: "impact-text",
4590
4971
  content: {
4591
- text,
4972
+ text: text2,
4592
4973
  style: {
4593
4974
  fontSize: fit.fontSize,
4594
4975
  fontFamily: getThemeFont(context, "title"),
@@ -4618,252 +4999,827 @@ function fullBleedQuote(input, context) {
4618
4999
  ];
4619
5000
  }
4620
5001
 
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;
5002
+ // src/doc/templates/conceptShapes.ts
5003
+ function region(id, d, fill, stroke, strokeWidth) {
4640
5004
  return {
4641
- markerWidthPx,
4642
- bodyIndentPx,
4643
- bodyWidthPx: Math.max(fontSize, textWidthPx - bodyIndentPx),
4644
- gapPx: LIST_ITEM_GAP_PX * (fontSize / baseFontSize)
5005
+ type: "path",
5006
+ id,
5007
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
5008
+ content: { d, fill, stroke, strokeWidth }
4645
5009
  };
4646
5010
  }
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;
5011
+ function disc(id, cx, cy, r, fill) {
5012
+ return {
5013
+ type: "shape",
5014
+ id,
5015
+ position: { x: cx - r, y: cy - r, width: r * 2, height: r * 2 },
5016
+ content: { shape: "circle", fill }
5017
+ };
4654
5018
  }
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 },
5019
+ function unbrokenSize(text2, size, width, floor) {
5020
+ const longest = Math.max(1, ...text2.split(/\s+/u).map((word) => word.length));
5021
+ return Math.max(Math.min(size, floor), Math.min(size, Math.floor(width / (longest * 0.64))));
5022
+ }
5023
+ function panel(id, left, top, width, height, pad, ruleH, accent, context) {
5024
+ const { theme } = context;
5025
+ return [
5026
+ [
4674
5027
  {
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
5028
+ type: "shape",
5029
+ id,
5030
+ position: { x: left, y: top, width, height },
5031
+ content: {
5032
+ shape: "rect",
5033
+ fill: withAlpha(theme.colors.text, 0.06),
5034
+ stroke: withAlpha(theme.colors.text, 0.12),
5035
+ strokeWidth: 2,
5036
+ borderRadius: Math.round(Math.min(width, height) * 0.06)
4685
5037
  }
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
- }
5038
+ },
5039
+ "surface",
5040
+ 0
5041
+ ],
5042
+ [
5043
+ {
5044
+ type: "shape",
5045
+ id: `${id}-rule`,
5046
+ position: { x: left + pad, y: top + pad, width: Math.min(width * 0.22, 64), height: ruleH },
5047
+ content: { shape: "rect", fill: accent, borderRadius: ruleH / 2 }
5048
+ },
5049
+ "rule",
5050
+ 0.2
5051
+ ]
5052
+ ];
5053
+ }
5054
+ var FIGURE_LINE_HEIGHT2 = 1.05;
5055
+ function statsBlock(input, context) {
5056
+ const crowded = asListWhenCrowded(input, context);
5057
+ if (crowded) return crowded;
5058
+ const stats = statItemsOf(input);
5059
+ if (conceptDrawsAs("stats", input.items) === "cards")
5060
+ return cardsBlock({ ...input, template: "cards" }, context);
5061
+ const area = frame(input, context);
5062
+ const { theme, viewport } = context;
5063
+ const W = area.right - area.left;
5064
+ const H = area.bottom - area.top;
5065
+ const n = stats.length;
5066
+ const columns2 = area.portrait ? n <= 3 ? 1 : 2 : n <= 4 ? n : 3;
5067
+ const rows = Math.ceil(n / columns2);
5068
+ const gutter = Math.round(viewport.width * (area.portrait ? 0.07 : 0.045));
5069
+ const rowGap = Math.round(viewport.height * 0.055);
5070
+ const cellW = (W - gutter * (columns2 - 1)) / columns2;
5071
+ const cellH = (H - rowGap * (rows - 1)) / rows;
5072
+ const figures = stats.map((stat) => splitFigure(stat.figure));
5073
+ const base = themedFontSize(
5074
+ rows > 1 && !area.portrait ? 124 : columns2 >= 4 ? 132 : columns2 === 3 ? 156 : 176,
5075
+ context,
5076
+ true
5077
+ );
5078
+ const floor = Math.min(base, themedFontSize(64, context, true));
5079
+ const figureSize = Math.min(
5080
+ ...figures.map(
5081
+ ({ number }) => number ? fitProse({
5082
+ text: number,
5083
+ baseFontSize: base,
5084
+ minFontSize: floor,
5085
+ maxWidthPx: cellW * 0.98,
5086
+ maxHeightPx: base * FIGURE_LINE_HEIGHT2,
5087
+ lineHeight: FIGURE_LINE_HEIGHT2
5088
+ }).fontSize : base
5089
+ )
5090
+ );
5091
+ const figureH = figureSize * FIGURE_LINE_HEIGHT2;
5092
+ const unitSize = Math.round(themedFontSize(LABEL_BASE, context, true) * (figureSize / base));
5093
+ const unitH = figures.some((figure) => figure.unit) ? unitSize * LABEL_LINE_HEIGHT : 0;
5094
+ const captionGap = Math.round(figureSize * 0.16);
5095
+ const captions = stats.map((stat) => ({ label: "", detail: stat.caption }));
5096
+ const { sizes } = fitRamp(
5097
+ captions,
5098
+ context,
5099
+ () => cellW,
5100
+ () => Math.max(0, cellH - figureH - unitH - captionGap)
5101
+ );
5102
+ const measures = captions.map((caption) => measureItem(caption, sizes, cellW));
5103
+ const captionH = Math.max(0, ...measures.map((m) => Number.isFinite(m.height) ? m.height : 0));
5104
+ const groupH = Math.min(cellH, figureH + unitH + captionGap + captionH);
5105
+ const gridH = rows * groupH + (rows - 1) * rowGap;
5106
+ const gridTop = area.top + Math.max(0, (H - gridH) * 0.38);
5107
+ const motion = getMotionProfile(context);
5108
+ const ruleW = Math.max(2, Math.round(viewport.width * 12e-4));
5109
+ const layers = [...area.layers];
5110
+ stats.forEach((stat, index) => {
5111
+ const row = Math.floor(index / columns2);
5112
+ const column = index % columns2;
5113
+ const inRow = Math.min(columns2, n - row * columns2);
5114
+ const rowLeft = area.left + (columns2 - inRow) * (cellW + gutter) / 2;
5115
+ const left = rowLeft + column * (cellW + gutter);
5116
+ const top = gridTop + row * (groupH + rowGap);
5117
+ const turn = FIRST_TURN + index * TURN_STAGGER;
5118
+ const { number, unit } = figures[index];
5119
+ const parts = [];
5120
+ if (column > 0)
5121
+ parts.push([
5122
+ {
5123
+ type: "shape",
5124
+ id: `stat-${index}-divider`,
5125
+ position: { x: left - gutter / 2 - ruleW / 2, y: top, width: ruleW, height: groupH },
5126
+ content: { shape: "rect", fill: withAlpha(theme.colors.text, 0.18) }
5127
+ },
5128
+ "bar",
5129
+ 0
5130
+ ]);
5131
+ const built = step(parts, index, context);
5132
+ if (number) {
5133
+ const count = motion.countUp ? countUpSpecFor(number) : null;
5134
+ built.push({
5135
+ ...textLayer(`stat-${index}`, number, box(left, top, cellW, figureH), {
5136
+ fontSize: figureSize,
5137
+ fontFamily: getThemeFont(context, "title"),
5138
+ fontWeight: "bold",
5139
+ color: area.accent,
5140
+ lineHeight: FIGURE_LINE_HEIGHT2,
5141
+ shadow: shouldUseShadow(context)
5142
+ }),
5143
+ buildStep: index,
5144
+ animation: count ? {
5145
+ type: "countUp",
5146
+ duration: motion.countUpDuration,
5147
+ delay: motionDelay(context, turn + 0.1),
5148
+ count
5149
+ } : entrance(context, "pop", turn + 0.1)
5150
+ });
4708
5151
  }
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)
5152
+ const rest = [];
5153
+ if (unit)
5154
+ rest.push([
5155
+ textLayer(`stat-${index}-unit`, unit, box(left, top + figureH, cellW, unitH), {
5156
+ fontSize: unitSize,
5157
+ fontFamily: getThemeFont(context, "title"),
5158
+ fontWeight: "bold",
5159
+ color: area.accent,
5160
+ lineHeight: LABEL_LINE_HEIGHT
5161
+ }),
5162
+ "text",
5163
+ 0.25
5164
+ ]);
5165
+ rest.push(
5166
+ ...words(
5167
+ itemText(
5168
+ `stat-${index}`,
5169
+ captions[index],
5170
+ measures[index],
5171
+ sizes,
5172
+ left,
5173
+ top + (number ? figureH + unitH + captionGap : 0),
5174
+ cellW,
5175
+ "left",
5176
+ context
5177
+ ),
5178
+ 0.35
5179
+ )
5180
+ );
5181
+ layers.push(...built, ...step(rest, index, context));
5182
+ });
5183
+ return layers;
5184
+ }
5185
+ var FUNNEL_STEM = 0.3;
5186
+ function tiers(kind, input, context) {
5187
+ const crowded = asListWhenCrowded(input, context);
5188
+ if (crowded) return crowded;
5189
+ const items = itemsOf(input);
5190
+ if (conceptDrawsAs(kind, input.items) === "cards")
5191
+ return cardsBlock({ ...input, template: "cards" }, context);
5192
+ const area = frame(input, context);
5193
+ const { theme, viewport } = context;
5194
+ const W = area.right - area.left;
5195
+ const H = area.bottom - area.top;
5196
+ const n = items.length;
5197
+ const portrait = area.portrait;
5198
+ const gap = Math.round(viewport.width * (portrait ? 0.02 : 0.045));
5199
+ const rowGap = Math.round(viewport.height * 0.02);
5200
+ const listGap = viewport.height * 0.045;
5201
+ const top = area.top;
5202
+ const listW = W - gap * 1.5;
5203
+ const listed = portrait ? fitRamp(
5204
+ items,
5205
+ context,
5206
+ () => listW,
5207
+ () => (H * 0.64 - listGap - rowGap * (n - 1)) / n
5208
+ ).sizes : void 0;
5209
+ const listH = listed ? items.reduce(
5210
+ (sum, item) => {
5211
+ const height = measureItem(item, listed, listW).height;
5212
+ return sum + (Number.isFinite(height) ? height : 0);
5213
+ },
5214
+ rowGap * (n - 1)
5215
+ ) : 0;
5216
+ const shapeH = Math.round(
5217
+ portrait ? Math.max(H * 0.36, Math.min(H * 0.5, H - listH - listGap)) : H
4721
5218
  );
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%" }
5219
+ const shapeW = Math.round(
5220
+ portrait ? Math.min(W * 0.78, shapeH * 1.5) : Math.min(W * 0.4, H * 1.1)
5221
+ );
5222
+ const shapeLeft = portrait ? area.left + (W - shapeW) / 2 : area.left;
5223
+ const cx = shapeLeft + shapeW / 2;
5224
+ const slit = Math.max(3, Math.round(viewport.height * 7e-3));
5225
+ const tierH = shapeH / n;
5226
+ const half = (y) => kind === "pyramid" ? shapeW / 2 * (y / shapeH) : shapeW / 2 * (1 - (1 - FUNNEL_STEM) * (y / shapeH));
5227
+ const tint = (index) => {
5228
+ const depth = n > 1 ? index / (n - 1) : 0;
5229
+ return withAlpha(area.accent, kind === "pyramid" ? 0.92 - 0.6 * depth : 0.32 + 0.6 * depth);
5230
+ };
5231
+ const textLeft = portrait ? area.left + gap * 1.5 : shapeLeft + shapeW + gap;
5232
+ const textW = area.right - textLeft;
5233
+ const textTop = portrait ? top + shapeH + listGap : top;
5234
+ const sizes = listed ?? fitRamp(
5235
+ items,
5236
+ context,
5237
+ () => textW,
5238
+ () => tierH - rowGap
5239
+ ).sizes;
5240
+ const measures = items.map((item) => measureItem(item, sizes, textW));
5241
+ const stroke = Math.max(2, Math.round(viewport.height * 3e-3));
5242
+ const shapes = [];
5243
+ const callouts = [];
5244
+ let rowTop = textTop;
5245
+ items.forEach((item, index) => {
5246
+ const y0 = index * tierH;
5247
+ const y1 = y0 + tierH - (index < n - 1 ? slit : 0);
5248
+ const [a, b] = [half(y0), half(y1)];
5249
+ shapes.push(
5250
+ ...step(
5251
+ [
5252
+ [
5253
+ region(
5254
+ `tier-${index}`,
5255
+ `M ${cx - a} ${top + y0} L ${cx + a} ${top + y0} L ${cx + b} ${top + y1} L ${cx - b} ${top + y1} Z`,
5256
+ tint(index),
5257
+ "none",
5258
+ 0
5259
+ ),
5260
+ "surface",
5261
+ 0
5262
+ ]
5263
+ ],
5264
+ index,
5265
+ context
5266
+ )
5267
+ );
5268
+ const m = measures[index];
5269
+ const height = Number.isFinite(m.height) ? m.height : tierH;
5270
+ const middle = top + (y0 + y1) / 2;
5271
+ const parts = [];
5272
+ let blockTop;
5273
+ if (portrait) {
5274
+ const firstLine = item.label ? sizes.label * LABEL_LINE_HEIGHT : sizes.plain * DETAIL_LINE_HEIGHT;
5275
+ const swatch = Math.round(sizes.label * 0.55);
5276
+ parts.push([
5277
+ {
5278
+ type: "shape",
5279
+ id: `tier-${index}-swatch`,
5280
+ position: {
5281
+ x: area.left,
5282
+ y: rowTop + firstLine / 2 - swatch / 2,
5283
+ width: swatch,
5284
+ height: swatch
5285
+ },
5286
+ content: { shape: "rect", fill: tint(index), borderRadius: Math.round(swatch * 0.2) }
5287
+ },
5288
+ "pop",
5289
+ 0.2
5290
+ ]);
5291
+ blockTop = rowTop;
5292
+ rowTop += height + rowGap;
5293
+ } else {
5294
+ const edge = cx + half((y0 + y1) / 2);
5295
+ const dot = Math.max(8, Math.round(viewport.height * 9e-3));
5296
+ parts.push(
5297
+ [
5298
+ connector(
5299
+ `tier-${index}-callout`,
5300
+ `M ${edge + dot} ${middle} L ${textLeft - gap * 0.25} ${middle}`,
5301
+ withAlpha(theme.colors.text, 0.45),
5302
+ stroke,
5303
+ false
5304
+ ),
5305
+ "draw",
5306
+ 0.2
5307
+ ],
5308
+ [
5309
+ disc(`tier-${index}-pin`, edge + dot / 2, middle, dot / 2, theme.colors.text),
5310
+ "pop",
5311
+ 0.15
5312
+ ]
5313
+ );
5314
+ blockTop = Math.max(area.top, middle - height / 2);
4732
5315
  }
4733
- ];
4734
- if (accentImage) {
4735
- layers.push(
4736
- ...createAccentLayers(
4737
- accentImage,
4738
- input.id,
4739
- themedImageTreatment(context, input.imageTreatment)
5316
+ parts.push(
5317
+ ...words(
5318
+ itemText(`tier-${index}`, item, m, sizes, textLeft, blockTop, textW, "left", context),
5319
+ 0.35
4740
5320
  )
4741
5321
  );
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
5322
+ callouts.push(...step(parts, index, context));
5323
+ });
5324
+ return [...area.layers, ...shapes, ...callouts];
5325
+ }
5326
+ function pyramidBlock(input, context) {
5327
+ return tiers("pyramid", input, context);
5328
+ }
5329
+ function funnelBlock(input, context) {
5330
+ return tiers("funnel", input, context);
5331
+ }
5332
+ function hubBlock(input, context) {
5333
+ const crowded = asListWhenCrowded(input, context);
5334
+ if (crowded) return crowded;
5335
+ const items = itemsOf(input);
5336
+ if (conceptDrawsAs("hub", input.items) === "cards")
5337
+ return cardsBlock({ ...input, template: "cards" }, context);
5338
+ const area = frame(input, context);
5339
+ const { viewport } = context;
5340
+ const W = area.right - area.left;
5341
+ const H = area.bottom - area.top;
5342
+ const n = items.length;
5343
+ const portrait = area.portrait;
5344
+ const center = String(input.center ?? "").replace(/\s+/gu, " ").trim();
5345
+ const hubR = Math.round(
5346
+ Math.min(viewport.height, viewport.width) * (center ? portrait ? 0.09 : 0.115 : 0.055)
4755
5347
  );
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) {
5348
+ const dot = Math.round(Math.min(viewport.height, viewport.width) * 0.034);
5349
+ const gap = Math.round(dot * 0.6);
5350
+ const cx = portrait ? area.left + hubR : (area.left + area.right) / 2;
5351
+ const cy = area.top + H / 2;
5352
+ const rx = portrait ? W * 0.4 : Math.min(H * 0.42, W * 0.2);
5353
+ const ry = portrait ? H * 0.42 : rx;
5354
+ const leftCount = portrait ? 0 : Math.ceil(n / 2);
5355
+ const spots = items.map((_, index) => {
5356
+ const onLeft = index < leftCount;
5357
+ const count = onLeft ? leftCount : n - leftCount;
5358
+ const at = onLeft ? index : index - leftCount;
5359
+ const spread = portrait ? 124 : count <= 1 ? 0 : count === 2 ? 70 : 110;
5360
+ const degrees = count <= 1 ? 0 : -spread / 2 + spread * at / (count - 1);
5361
+ const theta = degrees * Math.PI / 180;
5362
+ return {
5363
+ onLeft,
5364
+ x: cx + (onLeft ? -1 : 1) * rx * Math.cos(theta),
5365
+ y: cy + ry * Math.sin(theta)
5366
+ };
5367
+ });
5368
+ const labelW = (index) => spots[index].onLeft ? spots[index].x - dot / 2 - gap - area.left : area.right - (spots[index].x + dot / 2 + gap);
5369
+ const rowH = (index) => {
5370
+ const side = spots.filter((spot) => spot.onLeft === spots[index].onLeft);
5371
+ if (side.length < 2) return H * 0.8;
5372
+ return (side[side.length - 1].y - side[0].y) / (side.length - 1) * 0.9;
5373
+ };
5374
+ const scale = Math.min(
5375
+ ...items.map(
5376
+ (item, index) => fitRamp(
5377
+ [item],
5378
+ context,
5379
+ () => labelW(index),
5380
+ () => rowH(index)
5381
+ ).scale
5382
+ )
5383
+ );
5384
+ const sizes = ramp(context, scale);
5385
+ const measures = items.map((item, index) => measureItem(item, sizes, labelW(index)));
5386
+ const line = withAlpha(area.accent, 0.6);
5387
+ const stroke = Math.max(3, Math.round(viewport.height * 4e-3));
5388
+ const layers = [...area.layers];
5389
+ layers.push({
5390
+ ...disc("hub", cx, cy, hubR, area.accent),
5391
+ animation: entrance(context, "pop", 0.35)
5392
+ });
5393
+ if (center) {
5394
+ const side = hubR * 1.5;
5395
+ const floor = themedFontSize(18, context, true);
5396
+ const size = unbrokenSize(center, themedFontSize(36, context, true), side, floor);
5397
+ const fit = fitProse({
5398
+ text: center,
5399
+ baseFontSize: size,
5400
+ minFontSize: Math.min(size, floor),
5401
+ maxWidthPx: side,
5402
+ maxHeightPx: side,
5403
+ lineHeight: 1.15
5404
+ });
4767
5405
  layers.push({
4768
5406
  type: "text",
4769
- id: "list-title",
5407
+ id: "hub-center",
5408
+ position: { x: cx, y: cy, width: side, height: side, anchor: "center" },
4770
5409
  content: {
4771
- text: title,
5410
+ text: center,
4772
5411
  style: {
4773
- fontSize: titleFontSize,
5412
+ fontSize: fit.fontSize,
4774
5413
  fontFamily: getThemeFont(context, "title"),
4775
5414
  fontWeight: "bold",
4776
- color: theme.colors.text,
4777
- textAlign: "left",
4778
- shadow: shouldUseShadow(context)
5415
+ color: pickContrastingText(area.accent),
5416
+ textAlign: "center",
5417
+ verticalAlign: "middle",
5418
+ lineHeight: 1.15,
5419
+ shrinkToFit: true,
5420
+ ...fit.maxLines ? { maxLines: fit.maxLines } : {}
4779
5421
  }
4780
5422
  },
4781
- position: {
4782
- x: leftX,
4783
- y: adjustY(`${titleTopPct}%`, accentLayout),
4784
- width: accentLayout.textWidth
4785
- },
4786
- animation: motionEntrance(context, { type: "fadeIn", duration: 1 })
5423
+ animation: entrance(context, "pop", 0.35)
4787
5424
  });
4788
5425
  }
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)
4822
- }
4823
- },
4824
- position: {
4825
- x: leftX,
4826
- y: adjustY(`${itemY}%`, accentLayout),
4827
- width: markerWidthPx
4828
- },
4829
- animation
4830
- });
4831
- layers.push({
4832
- type: "text",
4833
- id: `item-${i}`,
4834
- content: {
4835
- text: itemText2,
4836
- ...itemHtml ? { html: itemHtml } : {},
4837
- paragraphs: [
4838
- {
4839
- text: itemText2,
4840
- list: { ordered, level: 0, ...ordered ? { number: i + 1 } : {} }
4841
- }
5426
+ items.forEach((item, index) => {
5427
+ const { x, y, onLeft } = spots[index];
5428
+ const length = Math.hypot(x - cx, y - cy) || 1;
5429
+ const [ux, uy] = [(x - cx) / length, (y - cy) / length];
5430
+ const from = hubR + dot * 0.35;
5431
+ const to = length - dot / 2 - dot * 0.15;
5432
+ const m = measures[index];
5433
+ const height = Number.isFinite(m.height) ? m.height : 0;
5434
+ const width = labelW(index);
5435
+ const left = onLeft ? x - dot / 2 - gap - width : x + dot / 2 + gap;
5436
+ const top = Math.min(
5437
+ Math.max(area.top, y - height / 2),
5438
+ Math.max(area.top, area.bottom - height)
5439
+ );
5440
+ layers.push(
5441
+ ...step(
5442
+ [
5443
+ [
5444
+ connector(
5445
+ `spoke-${index}`,
5446
+ `M ${cx + from * ux} ${cy + from * uy} L ${cx + to * ux} ${cy + to * uy}`,
5447
+ line,
5448
+ stroke,
5449
+ false
5450
+ ),
5451
+ "draw",
5452
+ 0
5453
+ ],
5454
+ [disc(`spoke-${index}-end`, x, y, dot / 2, area.accent), "pop", 0.3],
5455
+ ...words(
5456
+ itemText(
5457
+ `spoke-${index}`,
5458
+ item,
5459
+ m,
5460
+ sizes,
5461
+ left,
5462
+ top,
5463
+ width,
5464
+ onLeft ? "right" : "left",
5465
+ context
5466
+ ),
5467
+ 0.45
5468
+ )
4842
5469
  ],
4843
- flow: { id: LIST_ITEMS_FLOW },
4844
- 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)
4851
- }
5470
+ index,
5471
+ context
5472
+ )
5473
+ );
5474
+ });
5475
+ return layers;
5476
+ }
5477
+ function secondColor(accent, context) {
5478
+ const { colors, colorSchemes } = context.theme;
5479
+ const hue = (color) => isHex(color) ? hexHueDegrees(color) : NaN;
5480
+ const apart = (color) => {
5481
+ const turn = Math.abs(hue(color) - hue(accent)) % 360;
5482
+ return Math.min(turn, 360 - turn);
5483
+ };
5484
+ const candidates = [
5485
+ ...Object.values(colorSchemes ?? {}).map((scheme) => scheme.accent),
5486
+ colors.secondary,
5487
+ colors.primary,
5488
+ colors.highlight
5489
+ ];
5490
+ return candidates.find((color) => !!color && apart(color) >= 50) ?? colors.text;
5491
+ }
5492
+ function vennBlock(input, context) {
5493
+ const crowded = asListWhenCrowded(input, context);
5494
+ if (crowded) return crowded;
5495
+ const all = itemsOf(input);
5496
+ const sharedAt = vennOverlapIndex(all);
5497
+ const sets = all.map((item, index) => ({ item, index })).filter(({ index }) => index !== sharedAt);
5498
+ if (conceptDrawsAs("venn", input.items) === "cards")
5499
+ return cardsBlock({ ...input, template: "cards" }, context);
5500
+ const shared = sharedAt >= 0 ? all[sharedAt] : void 0;
5501
+ const area = frame(input, context);
5502
+ const { theme, viewport } = context;
5503
+ const W = area.right - area.left;
5504
+ const H = area.bottom - area.top;
5505
+ const portrait = area.portrait;
5506
+ const colors = [area.accent, secondColor(area.accent, context)];
5507
+ const mid = (area.left + area.right) / 2;
5508
+ const stroke = Math.max(2, Math.round(viewport.height * 3e-3));
5509
+ const gap = Math.round(viewport.width * 0.025);
5510
+ const layers = [...area.layers];
5511
+ const captionSize = themedFontSize(DETAIL_BASE * 1.1, context, false);
5512
+ const captionW = portrait ? W : W * 0.62;
5513
+ const captionLines = shared?.detail ? Math.min(3, estimateProseLineCount(shared.detail, captionSize, captionW)) : 0;
5514
+ const sharedLabelH = shared?.label && !portrait ? captionSize * 1.5 : 0;
5515
+ const band = shared && !portrait ? sharedLabelH + captionLines * captionSize * DETAIL_LINE_HEIGHT + viewport.height * 0.04 : 0;
5516
+ const circlesH = portrait ? H * 0.34 : H - band;
5517
+ const R = Math.round(Math.min(circlesH / 2, (portrait ? W : W * 0.34) / 2));
5518
+ const distance = R * 1.12;
5519
+ const cy = area.top + circlesH / 2;
5520
+ const centres = [mid - distance / 2, mid + distance / 2];
5521
+ const lensHalf = Math.sqrt(Math.max(0, R * R - (distance / 2) ** 2));
5522
+ const sideW = portrait ? W - gap * 1.5 : mid - distance / 2 - R - gap - area.left;
5523
+ const rowGap = Math.round(viewport.height * 0.025);
5524
+ const listTop = cy + R + viewport.height * 0.045;
5525
+ const listed = portrait ? [...sets.map(({ item }) => item), ...shared ? [shared] : []] : [];
5526
+ const { sizes } = fitRamp(
5527
+ portrait ? listed : sets.map(({ item }) => ({ label: "", detail: item.detail })),
5528
+ context,
5529
+ () => sideW,
5530
+ () => portrait ? (area.bottom - listTop - rowGap * (listed.length - 1)) / Math.max(1, listed.length) : circlesH * 0.9
5531
+ );
5532
+ let rowTop = listTop;
5533
+ const row = (id, item, fill) => {
5534
+ const m = measureItem(item, sizes, sideW);
5535
+ const firstLine = item.label ? sizes.label * LABEL_LINE_HEIGHT : sizes.plain * DETAIL_LINE_HEIGHT;
5536
+ const swatch = Math.round(sizes.label * 0.5);
5537
+ const parts = [
5538
+ [
5539
+ disc(`${id}-swatch`, area.left + swatch / 2, rowTop + firstLine / 2, swatch / 2, fill),
5540
+ "pop",
5541
+ 0.2
5542
+ ],
5543
+ ...words(
5544
+ itemText(id, item, m, sizes, area.left + gap * 1.5, rowTop, sideW, "left", context),
5545
+ 0.3
5546
+ )
5547
+ ];
5548
+ rowTop += (Number.isFinite(m.height) ? m.height : firstLine) + rowGap;
5549
+ return parts;
5550
+ };
5551
+ sets.forEach(({ item, index }, side) => {
5552
+ const cx = centres[side];
5553
+ const fill = colors[side];
5554
+ const parts = [
5555
+ [
5556
+ {
5557
+ type: "shape",
5558
+ id: `set-${side}`,
5559
+ position: { x: cx - R, y: cy - R, width: R * 2, height: R * 2 },
5560
+ content: {
5561
+ shape: "circle",
5562
+ fill: withAlpha(fill, 0.3),
5563
+ stroke: withAlpha(fill, 0.9),
5564
+ strokeWidth: stroke
5565
+ }
5566
+ },
5567
+ "pop",
5568
+ 0
5569
+ ]
5570
+ ];
5571
+ if (portrait) {
5572
+ parts.push(...row(`set-${side}`, item, withAlpha(fill, 0.75)));
5573
+ } else {
5574
+ const own = R * 2 - (R * 2 - distance);
5575
+ const lobeW = own * 0.74;
5576
+ const lobeX = side === 0 ? cx - R + own * 0.52 : cx + R - own * 0.52;
5577
+ if (item.label) {
5578
+ const floor = themedFontSize(20, context, true);
5579
+ const size = unbrokenSize(
5580
+ item.label,
5581
+ themedFontSize(LABEL_BASE, context, true),
5582
+ lobeW,
5583
+ floor
5584
+ );
5585
+ const fit = fitProse({
5586
+ text: item.label,
5587
+ baseFontSize: size,
5588
+ minFontSize: Math.min(size, floor),
5589
+ maxWidthPx: lobeW,
5590
+ maxHeightPx: R * 0.9,
5591
+ lineHeight: LABEL_LINE_HEIGHT
5592
+ });
5593
+ parts.push([
5594
+ {
5595
+ type: "text",
5596
+ id: `set-${side}-label`,
5597
+ position: { x: lobeX, y: cy, width: lobeW, height: R * 0.9, anchor: "center" },
5598
+ content: {
5599
+ text: item.label,
5600
+ style: {
5601
+ fontSize: fit.fontSize,
5602
+ fontFamily: getThemeFont(context, "title"),
5603
+ fontWeight: "bold",
5604
+ color: theme.colors.text,
5605
+ textAlign: "center",
5606
+ verticalAlign: "middle",
5607
+ lineHeight: LABEL_LINE_HEIGHT,
5608
+ shrinkToFit: true,
5609
+ ...fit.maxLines ? { maxLines: fit.maxLines } : {}
5610
+ }
5611
+ }
5612
+ },
5613
+ "text",
5614
+ 0.25
5615
+ ]);
5616
+ }
5617
+ if (item.detail) {
5618
+ const detail = { label: "", detail: item.detail };
5619
+ const m = measureItem(detail, sizes, sideW);
5620
+ const height = Number.isFinite(m.height) ? m.height : 0;
5621
+ const left = side === 0 ? area.left : area.right - sideW;
5622
+ parts.push(
5623
+ ...words(
5624
+ itemText(
5625
+ `set-${side}`,
5626
+ detail,
5627
+ m,
5628
+ sizes,
5629
+ left,
5630
+ Math.max(area.top, cy - height / 2),
5631
+ sideW,
5632
+ side === 0 ? "right" : "left",
5633
+ context
5634
+ ),
5635
+ 0.4
5636
+ )
5637
+ );
5638
+ }
5639
+ }
5640
+ layers.push(...step(parts, index, context));
5641
+ });
5642
+ if (shared) {
5643
+ const lens = region(
5644
+ "overlap",
5645
+ `M ${mid} ${cy - lensHalf} A ${R} ${R} 0 0 1 ${mid} ${cy + lensHalf} A ${R} ${R} 0 0 1 ${mid} ${cy - lensHalf} Z`,
5646
+ withAlpha(theme.colors.text, 0.16),
5647
+ withAlpha(theme.colors.text, 0.5),
5648
+ stroke
5649
+ );
5650
+ const parts = [[lens, "surface", 0]];
5651
+ if (portrait) {
5652
+ parts.push(...row("overlap", shared, withAlpha(theme.colors.text, 0.5)));
5653
+ } else {
5654
+ const top = cy + R + viewport.height * 0.02;
5655
+ parts.push([
5656
+ connector(
5657
+ "overlap-callout",
5658
+ `M ${mid} ${cy + lensHalf * 0.35} L ${mid} ${top}`,
5659
+ withAlpha(theme.colors.text, 0.5),
5660
+ stroke,
5661
+ false
5662
+ ),
5663
+ "draw",
5664
+ 0.2
5665
+ ]);
5666
+ if (shared.label)
5667
+ parts.push([
5668
+ textLayer(
5669
+ "overlap-label",
5670
+ shared.label,
5671
+ box(mid - captionW / 2, top + viewport.height * 0.012, captionW, sharedLabelH),
5672
+ {
5673
+ fontSize: captionSize,
5674
+ fontFamily: getThemeFont(context, "title"),
5675
+ fontWeight: "bold",
5676
+ color: theme.colors.text,
5677
+ lineHeight: LABEL_LINE_HEIGHT,
5678
+ textAlign: "center"
5679
+ }
5680
+ ),
5681
+ "text",
5682
+ 0.3
5683
+ ]);
5684
+ if (shared.detail)
5685
+ parts.push([
5686
+ textLayer(
5687
+ "overlap-detail",
5688
+ shared.detail,
5689
+ box(
5690
+ mid - captionW / 2,
5691
+ top + viewport.height * 0.012 + sharedLabelH,
5692
+ captionW,
5693
+ captionLines * captionSize * DETAIL_LINE_HEIGHT + captionSize * 0.4
5694
+ ),
5695
+ {
5696
+ fontSize: captionSize,
5697
+ fontFamily: getThemeFont(context, "body"),
5698
+ color: theme.colors.text,
5699
+ lineHeight: DETAIL_LINE_HEIGHT,
5700
+ textAlign: "center",
5701
+ maxLines: 3
5702
+ }
5703
+ ),
5704
+ "text",
5705
+ 0.4
5706
+ ]);
5707
+ }
5708
+ layers.push(...step(parts, sharedAt, context));
5709
+ }
5710
+ return layers;
5711
+ }
5712
+ var MATRIX_SCALES = [1.5, 1.32];
5713
+ function matrixBlock(input, context) {
5714
+ const crowded = asListWhenCrowded(input, context);
5715
+ if (crowded) return crowded;
5716
+ const items = itemsOf(input);
5717
+ if (conceptDrawsAs("matrix", input.items) === "cards")
5718
+ return cardsBlock({ ...input, template: "cards" }, context);
5719
+ const area = frame(input, context);
5720
+ const { theme, viewport } = context;
5721
+ const xAxis = String(input.xAxis ?? "").trim();
5722
+ const yAxis = String(input.yAxis ?? "").trim();
5723
+ const axisSize = themedFontSize(DETAIL_BASE * 1.05, context, false);
5724
+ const axisRoom = Math.round(axisSize * 1.9);
5725
+ const stroke = Math.max(3, Math.round(viewport.height * 4e-3));
5726
+ const left = area.left + (yAxis ? axisRoom * 0.7 : 0);
5727
+ const top = area.top + (yAxis ? axisRoom : 0);
5728
+ const right = area.right;
5729
+ const bottom = area.bottom - (xAxis ? axisRoom * 1.15 : 0);
5730
+ const gap = Math.round(viewport.width * 0.014);
5731
+ const cellW = (right - left - gap) / 2;
5732
+ const cellH = (bottom - top - gap) / 2;
5733
+ const pad = Math.round(Math.min(40, Math.max(22, cellW * 0.07)));
5734
+ const ruleH = Math.max(4, Math.round(viewport.height * 6e-3));
5735
+ const ruleGap = Math.round(viewport.height * 0.02);
5736
+ const chrome = pad * 2 + ruleH + ruleGap;
5737
+ const roomy = MATRIX_SCALES.find(
5738
+ (scale) => items.every(
5739
+ (item) => measureItem(item, ramp(context, scale), cellW - pad * 2).height <= (cellH - chrome) * 0.7
5740
+ )
5741
+ );
5742
+ const sizes = roomy ? ramp(context, roomy) : fitRamp(
5743
+ items,
5744
+ context,
5745
+ () => cellW - pad * 2,
5746
+ () => cellH - chrome
5747
+ ).sizes;
5748
+ const measures = items.map((item) => measureItem(item, sizes, cellW - pad * 2));
5749
+ const layers = [...area.layers];
5750
+ const axisColor = withAlpha(theme.colors.text, 0.6);
5751
+ const axisText = (id, text2, x, y, align) => textLayer(id, text2, box(x, y, (right - left) * 0.6, axisSize * 1.4), {
5752
+ fontSize: axisSize,
5753
+ fontFamily: getThemeFont(context, "title"),
5754
+ fontWeight: "bold",
5755
+ color: axisColor,
5756
+ lineHeight: 1.2,
5757
+ textAlign: align
5758
+ });
5759
+ if (yAxis) {
5760
+ const x = area.left + stroke;
5761
+ layers.push(
5762
+ {
5763
+ ...connector(
5764
+ "axis-y",
5765
+ `M ${x} ${bottom} L ${x} ${top + axisRoom * 0.1}`,
5766
+ axisColor,
5767
+ stroke
5768
+ ),
5769
+ animation: entrance(context, "draw", 0.3)
4852
5770
  },
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
5771
+ {
5772
+ ...axisText("axis-y-label", yAxis, area.left, area.top, "left"),
5773
+ animation: entrance(context, "text", 0.45)
5774
+ }
5775
+ );
5776
+ }
5777
+ if (xAxis) {
5778
+ const y = bottom + axisRoom * 0.35;
5779
+ layers.push(
5780
+ {
5781
+ ...connector("axis-x", `M ${left} ${y} L ${right - stroke} ${y}`, axisColor, stroke),
5782
+ animation: entrance(context, "draw", 0.3)
4861
5783
  },
4862
- animation
4863
- });
4864
- const itemHeightPx = lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT;
4865
- itemY += (itemHeightPx + gapPx) / context.viewport.height * 100;
5784
+ {
5785
+ ...axisText(
5786
+ "axis-x-label",
5787
+ xAxis,
5788
+ right - (right - left) * 0.6,
5789
+ y + axisRoom * 0.2,
5790
+ "right"
5791
+ ),
5792
+ animation: entrance(context, "text", 0.45)
5793
+ }
5794
+ );
4866
5795
  }
5796
+ items.forEach((item, index) => {
5797
+ const x = left + index % 2 * (cellW + gap);
5798
+ const y = top + Math.floor(index / 2) * (cellH + gap);
5799
+ layers.push(
5800
+ ...step(
5801
+ [
5802
+ ...panel(`cell-${index}`, x, y, cellW, cellH, pad, ruleH, area.accent, context),
5803
+ ...words(
5804
+ itemText(
5805
+ `cell-${index}`,
5806
+ item,
5807
+ measures[index],
5808
+ sizes,
5809
+ x + pad,
5810
+ y + pad + ruleH + ruleGap,
5811
+ cellW - pad * 2,
5812
+ "left",
5813
+ context
5814
+ ),
5815
+ 0.22
5816
+ )
5817
+ ],
5818
+ index,
5819
+ context
5820
+ )
5821
+ );
5822
+ });
4867
5823
  return layers;
4868
5824
  }
4869
5825
 
@@ -5176,6 +6132,8 @@ function definitionCard(input, context) {
5176
6132
  // src/doc/templates/comparisonBar.ts
5177
6133
  function comparisonBar(input, context) {
5178
6134
  const { leftLabel, rightLabel, unit, colorScheme = "blue" } = input;
6135
+ if (!leftLabel && !rightLabel && !Number.isFinite(input.leftValue) && !Number.isFinite(input.rightValue))
6136
+ return [];
5179
6137
  const { theme, viewport } = context;
5180
6138
  const colors = resolveColorScheme(context, colorScheme);
5181
6139
  const leftValue = Number.isFinite(input.leftValue) ? input.leftValue : 0;
@@ -5345,7 +6303,7 @@ var FIT_SLACK = 0.93;
5345
6303
  var QUOTE_LINE_HEIGHT = 1.35;
5346
6304
  var ATTR_LINE_HEIGHT = 1.4;
5347
6305
  function pullQuoteLockup(options, context) {
5348
- const { text, attribution, overMedia } = options;
6306
+ const { text: text2, attribution, overMedia } = options;
5349
6307
  const { theme, viewport } = context;
5350
6308
  const pct2 = (px) => px / viewport.height * 100;
5351
6309
  const quoteBaseSize = themedFontSize(52, context, true);
@@ -5364,7 +6322,7 @@ function pullQuoteLockup(options, context) {
5364
6322
  (frameHeightPx - attrHeightPx - gapReservePx) * FIT_SLACK
5365
6323
  );
5366
6324
  const fit = fitProse({
5367
- text,
6325
+ text: text2,
5368
6326
  baseFontSize: quoteBaseSize,
5369
6327
  minFontSize: quoteMinSize,
5370
6328
  maxWidthPx: columnWidthPx,
@@ -5388,7 +6346,7 @@ function pullQuoteLockup(options, context) {
5388
6346
  type: "text",
5389
6347
  id: "quote-text",
5390
6348
  content: {
5391
- text,
6349
+ text: text2,
5392
6350
  style: {
5393
6351
  fontSize: fit.fontSize,
5394
6352
  fontFamily: getThemeFont(context, "title"),
@@ -5466,7 +6424,7 @@ function pullQuoteOverlayLayer() {
5466
6424
  return createBackgroundLayer("overlay", "rgba(0, 0, 0, 0.55)");
5467
6425
  }
5468
6426
  function pullQuote(input, context) {
5469
- const { text, attribution, backgroundImage, ambientMotion } = input;
6427
+ const { text: text2, attribution, backgroundImage, ambientMotion } = input;
5470
6428
  const treatment = themedImageTreatment(context, input.imageTreatment);
5471
6429
  const layers = [];
5472
6430
  if (backgroundImage?.src) {
@@ -5496,7 +6454,7 @@ function pullQuote(input, context) {
5496
6454
  layers.push(createBackgroundLayer("bg", themedSurfaceGradient(context, 160)));
5497
6455
  }
5498
6456
  layers.push(
5499
- ...pullQuoteLockup({ text, attribution, overMedia: Boolean(backgroundImage?.src) }, context)
6457
+ ...pullQuoteLockup({ text: text2, attribution, overMedia: Boolean(backgroundImage?.src) }, context)
5500
6458
  );
5501
6459
  return layers;
5502
6460
  }
@@ -5639,7 +6597,7 @@ function videoWithCaption(input, context) {
5639
6597
 
5640
6598
  // src/doc/templates/videoPullQuote.ts
5641
6599
  function videoPullQuote(input, context) {
5642
- const { text, attribution, backgroundVideo } = input;
6600
+ const { text: text2, attribution, backgroundVideo } = input;
5643
6601
  const layers = [];
5644
6602
  if (backgroundVideo?.src) {
5645
6603
  layers.push(
@@ -5667,7 +6625,7 @@ function videoPullQuote(input, context) {
5667
6625
  layers.push(createBackgroundLayer("bg", themedSurfaceGradient(context, 160)));
5668
6626
  }
5669
6627
  layers.push(
5670
- ...pullQuoteLockup({ text, attribution, overMedia: Boolean(backgroundVideo?.src) }, context)
6628
+ ...pullQuoteLockup({ text: text2, attribution, overMedia: Boolean(backgroundVideo?.src) }, context)
5671
6629
  );
5672
6630
  return layers;
5673
6631
  }
@@ -5763,22 +6721,22 @@ function normalizeChartTable(table) {
5763
6721
  }
5764
6722
  function parseCellNumber(raw) {
5765
6723
  if (typeof raw !== "string") return null;
5766
- let text = raw.trim();
5767
- if (!text) return null;
6724
+ let text2 = raw.trim();
6725
+ if (!text2) return null;
5768
6726
  let negative = false;
5769
- const accounting = /^\((.*)\)$/.exec(text);
6727
+ const accounting = /^\((.*)\)$/.exec(text2);
5770
6728
  if (accounting) {
5771
6729
  negative = true;
5772
- text = accounting[1].trim();
6730
+ text2 = accounting[1].trim();
5773
6731
  }
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, "");
6732
+ text2 = text2.replace(/^([+-]?)\s*[$€£¥]\s*/, "$1");
6733
+ text2 = text2.replace(/%$/, "").trim();
6734
+ text2 = text2.replace(/,/g, "");
6735
+ if (/^[+-]?\d{1,3}(?: \d{3})+(?:\.\d+)?$/.test(text2)) {
6736
+ text2 = text2.replace(/ /g, "");
5779
6737
  }
5780
- if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(text)) return null;
5781
- const value = Number(text);
6738
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(text2)) return null;
6739
+ const value = Number(text2);
5782
6740
  if (!Number.isFinite(value)) return null;
5783
6741
  return negative ? -value : value;
5784
6742
  }
@@ -5794,13 +6752,13 @@ function isNumericColumn(rows, colIndex) {
5794
6752
  return numeric > 0 && numeric / Math.max(1, nonEmpty) >= NUMERIC_COLUMN_THRESHOLD;
5795
6753
  }
5796
6754
  function resolveColumnRef(ref, headers) {
5797
- const text = String(ref).trim();
5798
- if (!text) return null;
5799
- const lower = text.toLowerCase();
6755
+ const text2 = String(ref).trim();
6756
+ if (!text2) return null;
6757
+ const lower = text2.toLowerCase();
5800
6758
  const byName = headers.findIndex((h) => h.trim().toLowerCase() === lower);
5801
6759
  if (byName >= 0) return byName;
5802
- if (/^\d+$/.test(text)) {
5803
- const index = Number(text);
6760
+ if (/^\d+$/.test(text2)) {
6761
+ const index = Number(text2);
5804
6762
  if (index < headers.length) return index;
5805
6763
  }
5806
6764
  return null;
@@ -5848,9 +6806,9 @@ function buildChartData(table, options = {}) {
5848
6806
  }
5849
6807
 
5850
6808
  // 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;
6809
+ function niceNum(range2, round4) {
6810
+ const exponent = Math.floor(Math.log10(range2));
6811
+ const fraction = range2 / 10 ** exponent;
5854
6812
  let niceFraction;
5855
6813
  if (round4) {
5856
6814
  if (fraction < 1.5) niceFraction = 1;
@@ -5876,8 +6834,8 @@ function niceTicks(min, max, targetCount = 5) {
5876
6834
  if (min >= 0 && lo < 0) lo = 0;
5877
6835
  }
5878
6836
  const count = Math.max(2, targetCount);
5879
- const range = niceNum(hi - lo, false);
5880
- const step2 = niceNum(range / (count - 1), true);
6837
+ const range2 = niceNum(hi - lo, false);
6838
+ const step2 = niceNum(range2 / (count - 1), true);
5881
6839
  const niceMin = Math.floor(lo / step2) * step2;
5882
6840
  const niceMax = Math.ceil(hi / step2) * step2;
5883
6841
  const ticks = [];
@@ -5890,24 +6848,24 @@ function formatChartValue(value, unit) {
5890
6848
  if (!Number.isFinite(value)) return "\u2014";
5891
6849
  const sign = value < 0 ? "-" : "";
5892
6850
  const abs = Math.abs(value);
5893
- let text;
6851
+ let text2;
5894
6852
  if (abs >= 1e6) {
5895
- text = `${sign}${trimTrailingZero((abs / 1e6).toFixed(1))}M`;
6853
+ text2 = `${sign}${trimTrailingZero((abs / 1e6).toFixed(1))}M`;
5896
6854
  } else if (abs >= 1e3) {
5897
- text = `${sign}${trimTrailingZero((abs / 1e3).toFixed(abs >= 1e4 ? 0 : 1))}K`;
6855
+ text2 = `${sign}${trimTrailingZero((abs / 1e3).toFixed(abs >= 1e4 ? 0 : 1))}K`;
5898
6856
  } else {
5899
- text = `${sign}${abs.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
6857
+ text2 = `${sign}${abs.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
5900
6858
  }
5901
- return unit ? `${text} ${unit}` : text;
6859
+ return unit ? `${text2} ${unit}` : text2;
5902
6860
  }
5903
6861
  function formatChartDataValue(value, unit) {
5904
6862
  if (!Number.isFinite(value) || Math.abs(value) >= 1e4) return formatChartValue(value, unit);
5905
6863
  const sign = value < 0 ? "-" : "";
5906
- const text = `${sign}${Math.abs(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
5907
- return unit ? `${text} ${unit}` : text;
6864
+ const text2 = `${sign}${Math.abs(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
6865
+ return unit ? `${text2} ${unit}` : text2;
5908
6866
  }
5909
- function trimTrailingZero(text) {
5910
- return text.replace(/\.0$/, "");
6867
+ function trimTrailingZero(text2) {
6868
+ return text2.replace(/\.0$/, "");
5911
6869
  }
5912
6870
 
5913
6871
  // src/doc/templates/chart/geometry.ts
@@ -5944,15 +6902,15 @@ function arcPath(cx, cy, rOuter, rInner, startDeg, endDeg) {
5944
6902
  "Z"
5945
6903
  ].join(" ");
5946
6904
  }
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(" ");
6905
+ function polylinePath(points2) {
6906
+ if (points2.length === 0) return "";
6907
+ return points2.map((p, i) => `${i === 0 ? "M" : "L"} ${round(p.x)} ${round(p.y)}`).join(" ");
5950
6908
  }
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];
6909
+ function areaPath(points2, baselineY) {
6910
+ if (points2.length === 0) return "";
6911
+ const open = polylinePath(points2);
6912
+ const first = points2[0];
6913
+ const last = points2[points2.length - 1];
5956
6914
  return `${open} L ${round(last.x)} ${round(baselineY)} L ${round(first.x)} ${round(baselineY)} Z`;
5957
6915
  }
5958
6916
 
@@ -7092,6 +8050,48 @@ var BLOCK_MEDIA_LAYOUT_POLICIES = {
7092
8050
  unconsumedMedia: "reserved-slot",
7093
8051
  variants: TEXT_VARIANTS
7094
8052
  },
8053
+ stats: {
8054
+ summary: "Key figures by default; supplemental media is paired with the figures.",
8055
+ noMedia: "template-default",
8056
+ ownership: "supplemental",
8057
+ unconsumedMedia: "reserved-slot",
8058
+ variants: TEXT_VARIANTS
8059
+ },
8060
+ pyramid: {
8061
+ summary: "A drawn pyramid by default; supplemental media is paired with the tiers.",
8062
+ noMedia: "template-default",
8063
+ ownership: "supplemental",
8064
+ unconsumedMedia: "reserved-slot",
8065
+ variants: TEXT_VARIANTS
8066
+ },
8067
+ funnel: {
8068
+ summary: "A drawn funnel by default; supplemental media is paired with the stages.",
8069
+ noMedia: "template-default",
8070
+ ownership: "supplemental",
8071
+ unconsumedMedia: "reserved-slot",
8072
+ variants: TEXT_VARIANTS
8073
+ },
8074
+ hub: {
8075
+ summary: "A drawn hub and spokes by default; supplemental media is paired with the ideas.",
8076
+ noMedia: "template-default",
8077
+ ownership: "supplemental",
8078
+ unconsumedMedia: "reserved-slot",
8079
+ variants: TEXT_VARIANTS
8080
+ },
8081
+ venn: {
8082
+ summary: "Drawn overlapping circles by default; supplemental media is paired with the sets.",
8083
+ noMedia: "template-default",
8084
+ ownership: "supplemental",
8085
+ unconsumedMedia: "reserved-slot",
8086
+ variants: TEXT_VARIANTS
8087
+ },
8088
+ matrix: {
8089
+ summary: "A two-by-two grid by default; supplemental media is paired with the grid.",
8090
+ noMedia: "template-default",
8091
+ ownership: "supplemental",
8092
+ unconsumedMedia: "reserved-slot",
8093
+ variants: DENSE_VARIANTS
8094
+ },
7095
8095
  photoGrid: {
7096
8096
  summary: "Requires a primary image collection and owns the complete tiled media field.",
7097
8097
  noMedia: "unsupported",
@@ -7243,15 +8243,15 @@ function getBlockMediaLayoutPolicy(template) {
7243
8243
  }
7244
8244
 
7245
8245
  // src/doc/richMediaLayout.ts
7246
- function containedFeatureArea(region, aspects) {
8246
+ function containedFeatureArea(region2, aspects) {
7247
8247
  const count = aspects.length;
7248
8248
  const columns2 = Math.min(
7249
8249
  count,
7250
- Math.max(1, Math.ceil(Math.sqrt(count * region.width / region.height)))
8250
+ Math.max(1, Math.ceil(Math.sqrt(count * region2.width / region2.height)))
7251
8251
  );
7252
8252
  const rows = Math.ceil(count / columns2);
7253
- const width = region.width / columns2;
7254
- const height = region.height / rows;
8253
+ const width = region2.width / columns2;
8254
+ const height = region2.height / rows;
7255
8255
  return aspects.reduce((area, aspect) => {
7256
8256
  const ratio = aspect && Number.isFinite(aspect) && aspect > 0 ? aspect : 16 / 9;
7257
8257
  const containedWidth = Math.min(width, height * ratio);
@@ -7954,15 +8954,15 @@ var DIAGRAM_LABEL_LINE_HEIGHT = 1.25;
7954
8954
  var DIAGRAM_LABEL_HORIZONTAL_PADDING = 24;
7955
8955
  var DIAGRAM_LABEL_VERTICAL_PADDING = 16;
7956
8956
  var DIAGRAM_LABEL_MIN_FONT_SIZE = 10;
7957
- function fitDiagramLabel(text, boxWidth, boxHeight, preferredFontSize) {
8957
+ function fitDiagramLabel(text2, boxWidth, boxHeight, preferredFontSize) {
7958
8958
  const textWidth = Math.max(1, boxWidth - DIAGRAM_LABEL_HORIZONTAL_PADDING);
7959
8959
  const textHeight = Math.max(1, boxHeight - DIAGRAM_LABEL_VERTICAL_PADDING);
7960
8960
  const preferred = Math.max(DIAGRAM_LABEL_MIN_FONT_SIZE, Math.floor(preferredFontSize));
7961
8961
  let fontSize = preferred;
7962
- let lineCount = countWrappedLines(text, fontSize, textWidth);
8962
+ let lineCount = countWrappedLines(text2, fontSize, textWidth);
7963
8963
  while (fontSize > DIAGRAM_LABEL_MIN_FONT_SIZE && lineCount * fontSize * DIAGRAM_LABEL_LINE_HEIGHT > textHeight) {
7964
8964
  fontSize--;
7965
- lineCount = countWrappedLines(text, fontSize, textWidth);
8965
+ lineCount = countWrappedLines(text2, fontSize, textWidth);
7966
8966
  }
7967
8967
  return {
7968
8968
  fontSize,
@@ -7971,16 +8971,16 @@ function fitDiagramLabel(text, boxWidth, boxHeight, preferredFontSize) {
7971
8971
  textWidth
7972
8972
  };
7973
8973
  }
7974
- function countWrappedLines(text, fontSize, maxWidth) {
7975
- return text.split("\n").reduce((count, line) => count + wrappedLineCount2(line, fontSize, maxWidth), 0);
8974
+ function countWrappedLines(text2, fontSize, maxWidth) {
8975
+ return text2.split("\n").reduce((count, line) => count + wrappedLineCount2(line, fontSize, maxWidth), 0);
7976
8976
  }
7977
- function wrappedLineCount2(text, fontSize, maxWidth) {
7978
- if (!text.trim()) return 1;
8977
+ function wrappedLineCount2(text2, fontSize, maxWidth) {
8978
+ if (!text2.trim()) return 1;
7979
8979
  const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
7980
8980
  if (charsPerLine <= 0) return 1;
7981
8981
  let lines = 0;
7982
8982
  let currentLength = 0;
7983
- for (const word of text.split(/\s+/)) {
8983
+ for (const word of text2.split(/\s+/)) {
7984
8984
  const testLength = currentLength > 0 ? currentLength + 1 + word.length : word.length;
7985
8985
  if (testLength <= charsPerLine) {
7986
8986
  currentLength = testLength;
@@ -7999,6 +8999,7 @@ function wrappedLineCount2(text, fontSize, maxWidth) {
7999
8999
  }
8000
9000
 
8001
9001
  // src/doc/templates/diagramBlock.ts
9002
+ import { layoutGraph } from "@bendyline/squisq-graph";
8002
9003
  var NODE_WIDTH = 180;
8003
9004
  var NODE_HEIGHT = 64;
8004
9005
  var PADDING = 80;
@@ -8075,8 +9076,8 @@ function planCanvasFrame(template, title, context) {
8075
9076
  const { viewport } = context;
8076
9077
  const media = unconsumedMedia(context.block);
8077
9078
  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);
9079
+ const region2 = freeRegion(viewport, mediaRect);
9080
+ const innerWidth = Math.max(1, region2.width - PADDING * 2);
8080
9081
  let titleFit;
8081
9082
  if (title) {
8082
9083
  const baseFontSize = themedFontSize(40, context, true);
@@ -8103,7 +9104,7 @@ function planCanvasFrame(template, title, context) {
8103
9104
  maxWidthPx: widthPx,
8104
9105
  maxHeightPx: Math.min(
8105
9106
  CANVAS_DESCRIPTION_MAX_LINES * baseFontSize * CANVAS_DESCRIPTION_LINE_HEIGHT,
8106
- region.height * CANVAS_DESCRIPTION_MAX_HEIGHT_RATIO
9107
+ region2.height * CANVAS_DESCRIPTION_MAX_HEIGHT_RATIO
8107
9108
  ),
8108
9109
  lineHeight: CANVAS_DESCRIPTION_LINE_HEIGHT
8109
9110
  });
@@ -8112,13 +9113,13 @@ function planCanvasFrame(template, title, context) {
8112
9113
  const titleBlockPx = titleFit ? titleFit.heightPx + CANVAS_TITLE_GAP_PX : 0;
8113
9114
  const descriptionBlockPx = description ? CANVAS_DESCRIPTION_GAP_PX + description.fit.heightPx : 0;
8114
9115
  return {
8115
- region,
9116
+ region: region2,
8116
9117
  ...titleFit ? { title: titleFit } : {},
8117
9118
  ...description ? { description } : {},
8118
9119
  titleBlockPx,
8119
9120
  descriptionBlockPx,
8120
9121
  availW: innerWidth,
8121
- availH: Math.max(1, region.height - PADDING * 2 - titleBlockPx - descriptionBlockPx),
9122
+ availH: Math.max(1, region2.height - PADDING * 2 - titleBlockPx - descriptionBlockPx),
8122
9123
  ...mediaRect ? { mediaRect } : {}
8123
9124
  };
8124
9125
  }
@@ -8140,14 +9141,14 @@ function placeCanvasGroup(frame2, contentW, contentH) {
8140
9141
  descriptionY: offsetY + scaledH + CANVAS_DESCRIPTION_GAP_PX
8141
9142
  };
8142
9143
  }
8143
- function canvasTitleLayer(id, text, frame2, placement, context) {
9144
+ function canvasTitleLayer(id, text2, frame2, placement, context) {
8144
9145
  const fit = frame2.title;
8145
9146
  if (!fit) return void 0;
8146
9147
  return {
8147
9148
  type: "text",
8148
9149
  id,
8149
9150
  content: {
8150
- text,
9151
+ text: text2,
8151
9152
  style: {
8152
9153
  fontSize: fit.fontSize,
8153
9154
  fontFamily: getThemeFont(context, "title"),
@@ -8215,11 +9216,40 @@ function nodesFromChildren(context) {
8215
9216
  }))
8216
9217
  };
8217
9218
  }
8218
- function nodesFromTemplateData(input) {
9219
+ function placeDiagramNodes(nodes, rawEdges, direction) {
9220
+ if (nodes.length === 0 || nodes.some((node) => node.container)) return [];
9221
+ const ids = new Set(nodes.map((node) => node.id));
9222
+ const edges = (Array.isArray(rawEdges) ? rawEdges : []).filter(
9223
+ (edge) => typeof edge === "object" && edge !== null && ids.has(edge.source) && ids.has(edge.target) && edge.source !== edge.target
9224
+ ).map((edge, index) => ({ id: String(index), source: edge.source, target: edge.target }));
9225
+ const size = (node) => ({
9226
+ width: typeof node.w === "number" && node.w > 0 ? node.w : NODE_WIDTH,
9227
+ height: typeof node.h === "number" && node.h > 0 ? node.h : NODE_HEIGHT
9228
+ });
9229
+ const result = layoutGraph(
9230
+ { nodes: nodes.map((node) => ({ id: node.id, ...size(node) })), edges },
9231
+ {
9232
+ direction,
9233
+ layerSpacing: direction === "right" ? 100 : 96,
9234
+ nodeSpacing: direction === "right" ? 96 : 100,
9235
+ componentSpacing: 100,
9236
+ // The template draws its own connectors; only the positions are used.
9237
+ edgeSpacing: 1
9238
+ }
9239
+ );
9240
+ return nodes.map((node, index) => ({
9241
+ ...node,
9242
+ x: Math.round(result.nodes[index].x),
9243
+ y: Math.round(result.nodes[index].y)
9244
+ }));
9245
+ }
9246
+ function nodesFromTemplateData(input, direction) {
8219
9247
  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)
9248
+ const named = rawNodes.filter(
9249
+ (n) => typeof n === "object" && n !== null && typeof n.id === "string" && n.id.length > 0 && typeof n.label === "string"
8222
9250
  );
9251
+ const positioned = (n) => typeof n.x === "number" && Number.isFinite(n.x) && typeof n.y === "number" && Number.isFinite(n.y);
9252
+ const valid = named.some(positioned) ? named.filter(positioned) : placeDiagramNodes(named, input.edges, direction);
8223
9253
  const ids = new Set(valid.map((n) => n.id));
8224
9254
  const containerIds = new Set(
8225
9255
  valid.map((n) => n.container).filter((c) => typeof c === "string" && ids.has(c))
@@ -8264,7 +9294,7 @@ function diagramBlock(input, context) {
8264
9294
  const { theme, viewport, children = [] } = context;
8265
9295
  const colors = input.colorScheme ? resolveColorScheme(context, input.colorScheme) : void 0;
8266
9296
  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: [] };
9297
+ const resolved = children.length > 0 ? nodesFromChildren(context) : input.nodes && input.nodes.length > 0 ? nodesFromTemplateData(input, viewport.height > viewport.width ? "down" : "right") : { nodes: [], edges: [] };
8268
9298
  if (resolved.nodes.length === 0) {
8269
9299
  return [
8270
9300
  ...background,
@@ -8584,11 +9614,11 @@ function coerceTracks(raw) {
8584
9614
  const tracks = [];
8585
9615
  raw.forEach((candidate, trackIndex) => {
8586
9616
  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}`;
9617
+ const record2 = candidate;
9618
+ const id = typeof record2.id === "string" && record2.id.trim() ? record2.id.trim() : `track-${trackIndex + 1}`;
8589
9619
  const events = [];
8590
- if (Array.isArray(record.events)) {
8591
- record.events.forEach((eventCandidate, eventIndex) => {
9620
+ if (Array.isArray(record2.events)) {
9621
+ record2.events.forEach((eventCandidate, eventIndex) => {
8592
9622
  if (typeof eventCandidate !== "object" || eventCandidate === null) return;
8593
9623
  const event = eventCandidate;
8594
9624
  if (typeof event.label !== "string") return;
@@ -8612,8 +9642,8 @@ function coerceTracks(raw) {
8612
9642
  }
8613
9643
  tracks.push({
8614
9644
  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() } : {},
9645
+ ...typeof record2.label === "string" && record2.label.trim() ? { label: record2.label.trim() } : {},
9646
+ ...typeof record2.endLabel === "string" && record2.endLabel.trim() ? { endLabel: record2.endLabel.trim() } : {},
8617
9647
  events
8618
9648
  });
8619
9649
  });
@@ -9550,12 +10580,12 @@ function shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj
9550
10580
  }
9551
10581
  return out;
9552
10582
  }
9553
- function textLayer3(id, text, cx, cy, width, context, color, bold, fontAdj) {
10583
+ function textLayer3(id, text2, cx, cy, width, context, color, bold, fontAdj) {
9554
10584
  return {
9555
10585
  type: "text",
9556
10586
  id,
9557
10587
  content: {
9558
- text,
10588
+ text: text2,
9559
10589
  style: {
9560
10590
  fontSize: themedFontSize(Math.round((bold ? 22 : 18) * fontAdj), context, false),
9561
10591
  fontFamily: getThemeFont(context, "body"),
@@ -9660,7 +10690,7 @@ function computeLayoutLayers(children, _viewport, defaults = {}) {
9660
10690
  continue;
9661
10691
  }
9662
10692
  if (kind === "text") {
9663
- const text = bodyText2(child) || child.title || "";
10693
+ const text2 = bodyText2(child) || child.title || "";
9664
10694
  const layer = {
9665
10695
  id: child.id,
9666
10696
  type: "text",
@@ -9670,7 +10700,7 @@ function computeLayoutLayers(children, _viewport, defaults = {}) {
9670
10700
  width: numParam2(params, "width") ?? DEFAULT_TEXT.w,
9671
10701
  height: numParam2(params, "height") ?? DEFAULT_TEXT.h
9672
10702
  },
9673
- content: { text, style: textStyleFromParams(params, ink) }
10703
+ content: { text: text2, style: textStyleFromParams(params, ink) }
9674
10704
  };
9675
10705
  layers.push(layer);
9676
10706
  continue;
@@ -10006,12 +11036,12 @@ function handoffLayers(input, context, accent, textColor, bubbleFill) {
10006
11036
  }
10007
11037
  return layers;
10008
11038
  }
10009
- function nameLayer(id, text, x, fontSize, fontFamily, color) {
11039
+ function nameLayer(id, text2, x, fontSize, fontFamily, color) {
10010
11040
  return {
10011
11041
  type: "text",
10012
11042
  id,
10013
11043
  content: {
10014
- text,
11044
+ text: text2,
10015
11045
  style: { fontSize, fontFamily, fontWeight: "bold", color, textAlign: "center" }
10016
11046
  },
10017
11047
  position: { x, y: "42%", anchor: "center" }
@@ -10105,6 +11135,12 @@ var templateRegistry = {
10105
11135
  process: processBlock,
10106
11136
  cycle: cycleBlock,
10107
11137
  layers: layersBlock,
11138
+ stats: statsBlock,
11139
+ pyramid: pyramidBlock,
11140
+ funnel: funnelBlock,
11141
+ hub: hubBlock,
11142
+ venn: vennBlock,
11143
+ matrix: matrixBlock,
10108
11144
  photoGrid,
10109
11145
  definitionCard,
10110
11146
  comparisonBar,
@@ -10510,6 +11546,27 @@ var TEMPLATE_INPUT_DESCRIPTORS = {
10510
11546
  { key: "tracks", description: "Timeline tracks and events (supply via template data)" },
10511
11547
  { key: "links", description: "Branch links between event ids (supply via template data)" }
10512
11548
  ],
11549
+ cards: CONCEPT_DESCRIPTORS([
11550
+ {
11551
+ key: "versus",
11552
+ description: 'Set exactly two ideas against each other, with a "vs" mark between them',
11553
+ coerce: "boolean"
11554
+ }
11555
+ ]),
11556
+ process: CONCEPT_DESCRIPTORS(),
11557
+ cycle: CONCEPT_DESCRIPTORS(),
11558
+ layers: CONCEPT_DESCRIPTORS(),
11559
+ stats: CONCEPT_DESCRIPTORS(),
11560
+ pyramid: CONCEPT_DESCRIPTORS(),
11561
+ funnel: CONCEPT_DESCRIPTORS(),
11562
+ hub: CONCEPT_DESCRIPTORS([
11563
+ { key: "center", description: "The one to three words written inside the hub" }
11564
+ ]),
11565
+ venn: CONCEPT_DESCRIPTORS(),
11566
+ matrix: CONCEPT_DESCRIPTORS([
11567
+ { key: "xAxis", description: "What grows from left to right, named along the foot" },
11568
+ { key: "yAxis", description: "What grows from bottom to top, named up the side" }
11569
+ ]),
10513
11570
  videoWithCaption: [
10514
11571
  { key: "videoSrc", description: "Path to the video file", required: true },
10515
11572
  { key: "posterSrc", description: "Poster frame path" },
@@ -10522,6 +11579,17 @@ var TEMPLATE_INPUT_DESCRIPTORS = {
10522
11579
  { key: "videoLicense", description: "License identifier" }
10523
11580
  ]
10524
11581
  };
11582
+ function CONCEPT_DESCRIPTORS(extra = []) {
11583
+ return [
11584
+ { key: "title", description: "Headline above the ideas (defaults to the heading)" },
11585
+ {
11586
+ key: "colorScheme",
11587
+ description: "Accent color (defaults to the theme highlight)",
11588
+ valueHint: COLOR_SCHEME_HINT
11589
+ },
11590
+ ...extra
11591
+ ];
11592
+ }
10525
11593
  function FEATURE_DESCRIPTORS() {
10526
11594
  return [
10527
11595
  { key: "imageSrc", description: "Path to the feature image", required: true },
@@ -10726,9 +11794,9 @@ function fallbackBlockLayers(block, context, notice) {
10726
11794
  }
10727
11795
  function extractBodyText(block) {
10728
11796
  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`;
11797
+ const text2 = block.contents.filter((node) => !isPictureFence(node)).map((node) => extractPlainText(node)).join("\n").trim();
11798
+ if (text2.length <= MAX_BODY_CHARS) return text2;
11799
+ return `${text2.slice(0, MAX_BODY_CHARS).trimEnd()}\u2026`;
10732
11800
  }
10733
11801
 
10734
11802
  // src/doc/structuredData.ts
@@ -11056,6 +12124,21 @@ function materializeBlockLayersWithRuntime(block, options, runtime) {
11056
12124
  const context = runtime.templateContext ? cloneData(runtime.templateContext) : createTemplateContext(theme, blockIndex, totalBlocks, viewport, { motion: options.motion });
11057
12125
  context.block = cloneData(block);
11058
12126
  const execution = executeTemplateMaterialization(block, context, runtime.registry);
12127
+ if (execution.status === "ok" && drawsNothing(execution.layers) && hasContentToShow(block)) {
12128
+ const asContent = materializeBlockLayersWithRuntime(
12129
+ { ...block, template: "content" },
12130
+ options,
12131
+ runtime
12132
+ );
12133
+ return {
12134
+ ...asContent,
12135
+ diagnostic: {
12136
+ code: "template-drew-nothing",
12137
+ template: block.template,
12138
+ message: `Template "${block.template}" has nothing to draw here; the block is shown as content.`
12139
+ }
12140
+ };
12141
+ }
11059
12142
  if (execution.status === "ok") {
11060
12143
  const styledLayers = runtime.applyRenderStyle !== false ? applyRenderStyleToLayers(execution.layers, block, theme) : execution.layers;
11061
12144
  const layers = appendRichContentLayers(
@@ -11116,6 +12199,16 @@ function materializeBlockLayersWithRuntime(block, options, runtime) {
11116
12199
  diagnostic
11117
12200
  };
11118
12201
  }
12202
+ function drawsNothing(layers) {
12203
+ return layers.every(
12204
+ (layer) => (layer.type === "image" || layer.type === "video") && !layer.content.src
12205
+ );
12206
+ }
12207
+ function hasContentToShow(block) {
12208
+ if (resolveTemplateName(block.template) === "content") return false;
12209
+ const { title, contents } = block;
12210
+ return !!title?.trim() || (contents?.length ?? 0) > 0;
12211
+ }
11119
12212
  function mermaidSources(block) {
11120
12213
  return (block.contents ?? []).filter(
11121
12214
  (node) => node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid"
@@ -11249,15 +12342,15 @@ function collectRichMediaItems(layers, block) {
11249
12342
  ...embeddedSpatialMedia(block)
11250
12343
  ];
11251
12344
  }
11252
- function gridCells(region, count, viewport, inset) {
12345
+ function gridCells(region2, count, viewport, inset) {
11253
12346
  const unit = Math.min(viewport.width, viewport.height);
11254
12347
  const padding = inset ? unit * 0.025 : 0;
11255
12348
  const gap = count > 1 ? unit * 0.018 : 0;
11256
12349
  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)
12350
+ x: region2.x + padding,
12351
+ y: region2.y + padding,
12352
+ width: Math.max(0, region2.width - padding * 2),
12353
+ height: Math.max(0, region2.height - padding * 2)
11261
12354
  };
11262
12355
  const columns2 = Math.min(
11263
12356
  count,
@@ -11631,6 +12724,30 @@ var TEMPLATE_METADATA = {
11631
12724
  label: "Layers",
11632
12725
  description: "Draws nested parts as concentric rings, innermost first, each one called out."
11633
12726
  },
12727
+ stats: {
12728
+ label: "Key Numbers",
12729
+ description: "Sets two to six key figures side by side, each counting up over what it measures."
12730
+ },
12731
+ pyramid: {
12732
+ label: "Pyramid",
12733
+ description: "Stacks a hierarchy as a pyramid, the apex first and the base last."
12734
+ },
12735
+ funnel: {
12736
+ label: "Funnel",
12737
+ description: "Draws stages that narrow as a funnel, the widest first and the outcome last."
12738
+ },
12739
+ hub: {
12740
+ label: "Hub",
12741
+ description: "Arranges ideas around the one thing they share, each joined to it by a spoke."
12742
+ },
12743
+ venn: {
12744
+ label: "Venn",
12745
+ description: "Draws two sets as overlapping circles, with what they share in the overlap."
12746
+ },
12747
+ matrix: {
12748
+ label: "Matrix",
12749
+ description: "Places four ideas on two axes as a two-by-two grid."
12750
+ },
11634
12751
  photoGrid: {
11635
12752
  label: "Photo Grid",
11636
12753
  description: "Arranges up to four photos in a mosaic grid."
@@ -11844,6 +12961,42 @@ var TEMPLATE_AUTHORING_METADATA = {
11844
12961
  safeForContentFirst: false,
11845
12962
  placement: "heading"
11846
12963
  },
12964
+ stats: {
12965
+ role: "highlight",
12966
+ bodyPolicy: "derived",
12967
+ safeForContentFirst: false,
12968
+ placement: "heading"
12969
+ },
12970
+ pyramid: {
12971
+ role: "content",
12972
+ bodyPolicy: "derived",
12973
+ safeForContentFirst: false,
12974
+ placement: "heading"
12975
+ },
12976
+ funnel: {
12977
+ role: "content",
12978
+ bodyPolicy: "derived",
12979
+ safeForContentFirst: false,
12980
+ placement: "heading"
12981
+ },
12982
+ hub: {
12983
+ role: "content",
12984
+ bodyPolicy: "derived",
12985
+ safeForContentFirst: false,
12986
+ placement: "heading"
12987
+ },
12988
+ venn: {
12989
+ role: "comparison",
12990
+ bodyPolicy: "derived",
12991
+ safeForContentFirst: false,
12992
+ placement: "heading"
12993
+ },
12994
+ matrix: {
12995
+ role: "comparison",
12996
+ bodyPolicy: "derived",
12997
+ safeForContentFirst: false,
12998
+ placement: "heading"
12999
+ },
11847
13000
  photoGrid: {
11848
13001
  role: "media",
11849
13002
  bodyPolicy: "structured",
@@ -11960,12 +13113,217 @@ var TEMPLATE_AUTHORING_METADATA = {
11960
13113
  }
11961
13114
  };
11962
13115
 
13116
+ // src/transform/presentationPlan.ts
13117
+ var PRESENTATION_KEY = "squisq-presentation";
13118
+ var PRESENTATION_LAYOUTS = [
13119
+ "title",
13120
+ "divider",
13121
+ "statement",
13122
+ "emphasis",
13123
+ "quote",
13124
+ "stat",
13125
+ "definition",
13126
+ "list",
13127
+ "comparison",
13128
+ "steps",
13129
+ "image",
13130
+ "cards",
13131
+ "process",
13132
+ "cycle",
13133
+ "layers",
13134
+ "stats",
13135
+ "pyramid",
13136
+ "funnel",
13137
+ "hub",
13138
+ "venn",
13139
+ "matrix"
13140
+ ];
13141
+ var AUTHORED_PRESENTATION_LAYOUT = "authored";
13142
+ var EMPHASIS_CHARACTERS = 60;
13143
+ var spaced = (text2) => text2.replace(/\s+/gu, " ").trim();
13144
+ function presentationStat(headline, points2) {
13145
+ for (const [index, line] of [headline, ...points2].entries()) {
13146
+ const match = matchNumberHighlight(line);
13147
+ if (match) return { match, line, index: index - 1 };
13148
+ }
13149
+ return null;
13150
+ }
13151
+ function presentationLayoutFits(layout, headline, points2, passage) {
13152
+ return presentationLayoutProblem(layout, headline, points2, passage) === void 0;
13153
+ }
13154
+ function presentationLayoutProblem(layout, headline, points2, passage) {
13155
+ const concept = conceptTemplate(layout);
13156
+ if (concept) return conceptItemsProblem(concept, points2, { max: PRESENTATION_LIMITS.points });
13157
+ switch (layout) {
13158
+ case "list":
13159
+ case "steps":
13160
+ return points2.length >= 2 ? void 0 : `${layout} takes at least 2 points and this has ${points2.length}`;
13161
+ case "comparison":
13162
+ return points2.length === 2 ? void 0 : `comparison takes exactly 2 points and this has ${points2.length}`;
13163
+ case "definition":
13164
+ return points2.length >= 1 ? void 0 : "definition takes a point that defines its headline";
13165
+ case "stat":
13166
+ 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";
13167
+ case "emphasis":
13168
+ return headline.length <= EMPHASIS_CHARACTERS ? void 0 : `emphasis takes a headline of at most ${EMPHASIS_CHARACTERS} characters`;
13169
+ case "quote":
13170
+ return spaced(passage).includes(spaced(headline)) ? void 0 : "quote takes a headline copied word for word from the passage";
13171
+ default:
13172
+ return void 0;
13173
+ }
13174
+ }
13175
+ function presentationLayoutNeeds(layout, wording = {}) {
13176
+ const points2 = wording.points ?? "points";
13177
+ const concept = conceptTemplate(layout);
13178
+ if (concept) {
13179
+ const count = `${conceptItemCount(concept, { max: PRESENTATION_LIMITS.points })} ${points2}`;
13180
+ if (concept === "stats") return `${count}, each stating a figure`;
13181
+ if (concept === "venn") return `${count}, one of them labelled "Both:"`;
13182
+ return count;
13183
+ }
13184
+ switch (layout) {
13185
+ case "list":
13186
+ case "steps":
13187
+ return `at least 2 ${points2}`;
13188
+ case "comparison":
13189
+ return `exactly 2 ${points2}`;
13190
+ case "stat":
13191
+ return "a prominent figure, such as 40% or $3 million";
13192
+ case "emphasis":
13193
+ return `a headline of ${EMPHASIS_CHARACTERS} characters or fewer`;
13194
+ case "quote":
13195
+ return "a sentence of 110 characters or fewer to quote";
13196
+ case "image":
13197
+ return "a picture";
13198
+ default:
13199
+ return void 0;
13200
+ }
13201
+ }
13202
+ var BEAT_EXTRAS = ["center", "xAxis", "yAxis"];
13203
+ var LAYOUT_EXTRAS = {
13204
+ hub: ["center"],
13205
+ matrix: ["xAxis", "yAxis"]
13206
+ };
13207
+ function presentationBeatExtras(layout, source) {
13208
+ const extras = {};
13209
+ if (!record(source)) return extras;
13210
+ for (const key of LAYOUT_EXTRAS[layout] ?? []) {
13211
+ const value = source[key];
13212
+ const clean = typeof value === "string" ? value.replace(/\s+/gu, " ").trim() : "";
13213
+ if (clean && text(clean, PRESENTATION_LIMITS.extraCharacters)) extras[key] = clean;
13214
+ }
13215
+ return extras;
13216
+ }
13217
+ var PRESENTATION_LIMITS = {
13218
+ sourceCharacters: 4e4,
13219
+ beats: 80,
13220
+ serializedCharacters: 16e4,
13221
+ mediaPerBeat: 4,
13222
+ /** The most points a summary slide holds. */
13223
+ points: 4,
13224
+ /** The longest a hub's centre or a matrix's axis name may be. */
13225
+ extraCharacters: 40
13226
+ };
13227
+ function record(value) {
13228
+ return !!value && typeof value === "object" && !Array.isArray(value);
13229
+ }
13230
+ function keys(value, required, optional = []) {
13231
+ return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
13232
+ }
13233
+ function text(value, max) {
13234
+ return typeof value === "string" && value.trim().length > 0 && value.length <= max && !Array.from(value).some((character) => {
13235
+ const code = character.charCodeAt(0);
13236
+ return code < 32 && code !== 9 && code !== 10 && code !== 13;
13237
+ });
13238
+ }
13239
+ function parsePresentationPlan(input) {
13240
+ let value = input;
13241
+ if (typeof value === "string") {
13242
+ if (value.length > PRESENTATION_LIMITS.serializedCharacters) return null;
13243
+ try {
13244
+ value = JSON.parse(value);
13245
+ } catch {
13246
+ return null;
13247
+ }
13248
+ }
13249
+ 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)
13250
+ return null;
13251
+ const beats = [];
13252
+ const ids = /* @__PURE__ */ new Set();
13253
+ let end = 0;
13254
+ for (const beat of value.beats) {
13255
+ if (!record(beat) || !keys(
13256
+ beat,
13257
+ ["id", "sourceStart", "sourceEnd", "layout", "headline", "points"],
13258
+ ["imageSrc", "media", "hidden", ...BEAT_EXTRAS]
13259
+ ) || "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)))
13260
+ return null;
13261
+ const extras = presentationBeatExtras(String(beat.layout), beat);
13262
+ if (BEAT_EXTRAS.some((key) => key in beat && beat[key] !== extras[key])) return null;
13263
+ if (beat.layout === AUTHORED_PRESENTATION_LAYOUT) {
13264
+ if (beat.points.length || "media" in beat) return null;
13265
+ } else if (!PRESENTATION_LAYOUTS.includes(beat.layout) || !presentationLayoutFits(
13266
+ beat.layout,
13267
+ beat.headline,
13268
+ beat.points,
13269
+ value.sourceText.slice(beat.sourceStart, beat.sourceEnd)
13270
+ ))
13271
+ return null;
13272
+ if (beat.layout === "image" ? !text(beat.imageSrc, 2048) : "imageSrc" in beat) return null;
13273
+ 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))))
13274
+ return null;
13275
+ ids.add(beat.id);
13276
+ end = beat.sourceEnd;
13277
+ beats.push({
13278
+ id: beat.id,
13279
+ sourceStart: beat.sourceStart,
13280
+ sourceEnd: beat.sourceEnd,
13281
+ layout: beat.layout,
13282
+ headline: beat.headline,
13283
+ points: [...beat.points],
13284
+ ...beat.layout === "image" ? { imageSrc: beat.imageSrc } : {},
13285
+ ..."media" in beat ? { media: [...beat.media] } : {},
13286
+ ...beat.hidden === true ? { hidden: true } : {},
13287
+ ...extras
13288
+ });
13289
+ }
13290
+ if (value.sourceText.slice(end).trim()) return null;
13291
+ return {
13292
+ version: 1,
13293
+ sourceText: value.sourceText,
13294
+ origin: value.origin,
13295
+ beats
13296
+ };
13297
+ }
13298
+ function serializePresentationPlan(plan) {
13299
+ const parsed = parsePresentationPlan(plan);
13300
+ if (!parsed) throw new Error("The presentation plan is invalid.");
13301
+ const result = JSON.stringify(parsed);
13302
+ if (result.length > PRESENTATION_LIMITS.serializedCharacters)
13303
+ throw new Error("The presentation plan is too large.");
13304
+ return result;
13305
+ }
13306
+
11963
13307
  // src/doc/templates/authoringGuide.ts
11964
13308
  var CHART = {
11965
13309
  fields: "title, headers [label, value], rows [[label, value], \u2026] 2\u20136 rows, showValues: true",
11966
13310
  use: "only values the {source} states"
11967
13311
  };
11968
13312
  var CONCEPT_BUILD = 'build: {mode: "items"} to reveal them as they are spoken';
13313
+ function conceptFields(template, items) {
13314
+ const spec = CONCEPT_SPECS[template];
13315
+ const inputs = spec.inputs ?? [];
13316
+ const form = spec.itemForm === "figure-caption" ? '"Figure: what it measures"' : '"Label: detail"';
13317
+ return [
13318
+ "title",
13319
+ // What names the picture comes before its items; a switch comes after.
13320
+ ...inputs.filter((input) => input.key !== "versus").map((input) => input.guide),
13321
+ items ?? `items: [${form}, \u2026] ${conceptItemCount(template)}${spec.order ? ` ${spec.order}` : ""}`,
13322
+ ...inputs.filter((input) => input.key === "versus").map((input) => input.guide),
13323
+ CONCEPT_BUILD
13324
+ ].join(", ");
13325
+ }
13326
+ var points = (template) => conceptItemCount(template, { max: PRESENTATION_LIMITS.points });
11969
13327
  var TEMPLATE_GUIDES = {
11970
13328
  imageWithCaption: {
11971
13329
  fields: "imageSrc, imageAlt, caption \u2264 90 chars, ambientMotion: zoomIn|zoomOut|panLeft|panRight",
@@ -12002,7 +13360,7 @@ var TEMPLATE_GUIDES = {
12002
13360
  use: "3\u20137 dated events the {source} states"
12003
13361
  },
12004
13362
  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 }]",
13363
+ 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
13364
  use: "relationships the {source} describes"
12007
13365
  },
12008
13366
  list: {
@@ -12021,24 +13379,54 @@ var TEMPLATE_GUIDES = {
12021
13379
  use: "opens a section on a photograph"
12022
13380
  },
12023
13381
  cards: {
12024
- fields: `title, items: ["Label: detail", \u2026] 2\u20134, versus: true for exactly two set against each other, ${CONCEPT_BUILD}`,
13382
+ fields: conceptFields("cards"),
12025
13383
  use: "parallel ideas side by side, such as kinds, causes or features"
12026
13384
  },
12027
13385
  process: {
12028
- fields: `title, items: ["Label: detail", \u2026] 2\u20134 in order, ${CONCEPT_BUILD}`,
13386
+ fields: conceptFields("process"),
12029
13387
  use: "ordered stages or dated events, drawn as numbered steps joined by arrows"
12030
13388
  },
12031
13389
  cycle: {
12032
- fields: `title, items: ["Label: detail", \u2026] 3\u20135, ${CONCEPT_BUILD}`,
13390
+ fields: conceptFields("cycle"),
12033
13391
  use: "stages that repeat in a loop, drawn on a ring"
12034
13392
  },
12035
13393
  layers: {
12036
- fields: `title, items: ["Label: detail", \u2026] 3\u20135 innermost first, ${CONCEPT_BUILD}`,
13394
+ fields: conceptFields("layers"),
12037
13395
  use: "parts nested inside one another, drawn as concentric rings"
13396
+ },
13397
+ stats: {
13398
+ fields: conceptFields(
13399
+ "stats",
13400
+ `items: ["Figure: what it measures", \u2026] ${conceptItemCount("stats")} such as "40%: of trips made on foot"`
13401
+ ),
13402
+ use: "several key numbers the {source} states, side by side; each counts up"
13403
+ },
13404
+ pyramid: {
13405
+ fields: conceptFields("pyramid"),
13406
+ use: "a hierarchy, or levels that rest on one another, drawn as a pyramid"
13407
+ },
13408
+ funnel: {
13409
+ fields: conceptFields("funnel"),
13410
+ use: "stages that narrow from many to few, drawn as a funnel"
13411
+ },
13412
+ hub: {
13413
+ fields: conceptFields("hub"),
13414
+ use: "factors, causes or parts of the one thing `center` names, drawn around a hub"
13415
+ },
13416
+ venn: {
13417
+ fields: conceptFields(
13418
+ "venn",
13419
+ 'items: ["A: what only A has", "B: what only B has", "Both: what they share"]'
13420
+ ),
13421
+ use: "two things compared by what each has alone and what they share, drawn as overlapping circles"
13422
+ },
13423
+ matrix: {
13424
+ fields: conceptFields("matrix"),
13425
+ use: "four ideas placed on two axes, such as effort against impact"
12038
13426
  }
12039
13427
  };
12040
- function guideText(text, wording = {}) {
12041
- return text.replace(/\{source\}/gu, wording.source ?? "source");
13428
+ function guideText(text2, wording = {}) {
13429
+ return text2.replace(/\{source\}/gu, wording.source ?? "source");
12042
13430
  }
12043
13431
  function templateCatalog(entries, wording = {}) {
12044
13432
  return entries.map((entry) => {
@@ -12059,13 +13447,51 @@ var PRESENTATION_LAYOUT_GUIDE = {
12059
13447
  comparison: "exactly 2 genuinely contrasting points",
12060
13448
  steps: "2\u20133 ordered actions",
12061
13449
  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"
13450
+ cards: `${points("cards")} parallel ideas side by side, such as kinds, causes or features`,
13451
+ process: `${points("process")} ordered stages or dated events, drawn as numbered steps joined by arrows`,
13452
+ cycle: `${points("cycle")} stages that repeat in a loop, drawn on a ring`,
13453
+ layers: `${points("layers")} parts nested inside one another, innermost first, such as a core and the layers around it, drawn as concentric rings`,
13454
+ 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`,
13455
+ pyramid: `${points("pyramid")} levels of a hierarchy, the top first and the base last, drawn as a pyramid`,
13456
+ funnel: `${points("funnel")} stages that narrow from many to few, the widest first, drawn as a funnel`,
13457
+ hub: `${points("hub")} factors, causes or parts of one thing, drawn around a hub; add "center", that thing in 1\u20133 words`,
13458
+ venn: `${points("venn")} points: two things compared, each "Name: what only it has", then "Both: what they share"; drawn as overlapping circles`,
13459
+ 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`
13460
+ };
13461
+ var PRESENTATION_LAYOUT_LABELS = {
13462
+ title: { label: "Title", description: "Opens the deck or closes it." },
13463
+ divider: { label: "Section break", description: "A new topic begins; only the headline." },
13464
+ statement: { label: "Statement", description: "One idea under the headline." },
13465
+ emphasis: { label: "Big statement", description: "One short takeaway in display type." },
13466
+ quote: { label: "Quote", description: "One sentence in the passage\u2019s own words." },
13467
+ stat: { label: "Key number", description: "One figure, large, beside what it means." },
13468
+ definition: { label: "Definition", description: "A term and what it means." },
13469
+ list: { label: "Points", description: "Two or three points side by side." },
13470
+ comparison: { label: "Comparison", description: "Two sides set against each other." },
13471
+ steps: { label: "Steps", description: "Ordered actions, numbered and joined by arrows." },
13472
+ image: { label: "Picture", description: "A picture with the headline as its caption." },
13473
+ cards: { label: "Cards", description: "Parallel ideas side by side." },
13474
+ process: { label: "Process", description: "Ordered stages; dated ones draw as a timeline." },
13475
+ cycle: { label: "Cycle", description: "Stages on a ring that comes back to its start." },
13476
+ layers: { label: "Layers", description: "Nested parts as concentric rings." },
13477
+ stats: { label: "Key numbers", description: "Several figures side by side, counting up." },
13478
+ pyramid: { label: "Pyramid", description: "Levels of a hierarchy, the top first." },
13479
+ funnel: { label: "Funnel", description: "Stages that narrow from many to few." },
13480
+ hub: { label: "Hub", description: "Ideas around the one thing they share." },
13481
+ venn: { label: "Venn", description: "Two sets and what they share." },
13482
+ matrix: { label: "Matrix", description: "Four ideas on two axes." }
12066
13483
  };
12067
- var DRAWN = ["cards", "process", "cycle", "layers"];
12068
- var NUMBERS = ["statHighlight", "comparisonBar", "columnChart", "barChart", "pieChart"];
13484
+ var DRAWN_CORE = ["cards", "process", "cycle", "layers"];
13485
+ var DRAWN_SHAPES = ["pyramid", "funnel", "hub", "venn", "matrix"];
13486
+ var DRAWN = [...DRAWN_CORE, ...DRAWN_SHAPES];
13487
+ var NUMBERS = [
13488
+ "statHighlight",
13489
+ "stats",
13490
+ "comparisonBar",
13491
+ "columnChart",
13492
+ "barChart",
13493
+ "pieChart"
13494
+ ];
12069
13495
  var ACCENTED = [
12070
13496
  "factCard",
12071
13497
  "statHighlight",
@@ -12081,7 +13507,9 @@ var AUTHORING_TRICKS = [
12081
13507
  },
12082
13508
  {
12083
13509
  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.",
13510
+ 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.",
13511
+ 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".',
13512
+ 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
13513
  templates: [...NUMBERS, "timeline"]
12086
13514
  },
12087
13515
  {
@@ -12092,8 +13520,16 @@ var AUTHORING_TRICKS = [
12092
13520
  {
12093
13521
  id: "draw-ideas",
12094
13522
  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
13523
+ 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.`,
13524
+ 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".',
13525
+ templates: DRAWN_CORE
13526
+ },
13527
+ {
13528
+ id: "draw-shapes",
13529
+ 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.',
13530
+ 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.',
13531
+ 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.',
13532
+ templates: DRAWN_SHAPES
12097
13533
  },
12098
13534
  {
12099
13535
  id: "reveal-as-spoken",
@@ -12117,7 +13553,8 @@ var AUTHORING_TRICKS = [
12117
13553
  },
12118
13554
  {
12119
13555
  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."
13556
+ 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.",
13557
+ 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
13558
  }
12122
13559
  ];
12123
13560
  function authoringTricks(options = {}) {
@@ -12126,8 +13563,8 @@ function authoringTricks(options = {}) {
12126
13563
  return selected.filter(
12127
13564
  (trick) => !offered || !trick.templates || trick.templates.some((template) => offered.has(template))
12128
13565
  ).flatMap((trick) => {
12129
- const text = options.surface === "summary" ? trick.summary : trick.text;
12130
- return text ? [guideText(text, options.wording)] : [];
13566
+ const text2 = options.surface === "summary" ? trick.summary : options.surface === "storyboard" ? trick.storyboard : trick.text;
13567
+ return text2 ? [guideText(text2, options.wording)] : [];
12131
13568
  });
12132
13569
  }
12133
13570
 
@@ -12157,10 +13594,10 @@ function coverScrimGradient(background) {
12157
13594
  const stops = COVER_SCRIM_STOPS.map((s) => `${withAlpha(background, s.alpha)} ${s.pos}%`);
12158
13595
  return `linear-gradient(0deg, ${stops.join(", ")})`;
12159
13596
  }
12160
- function fitFrostedText(text, size, minSize, maxPlateWidth, maxLines) {
13597
+ function fitFrostedText(text2, size, minSize, maxPlateWidth, maxLines) {
12161
13598
  const wrapAt = (s) => {
12162
13599
  const column = maxPlateWidth - 2 * FROSTED_PAD_X_EM * s;
12163
- const lines = text.split("\n").flatMap((line) => balancedDisplayLines(line, s, column));
13600
+ const lines = text2.split("\n").flatMap((line) => balancedDisplayLines(line, s, column));
12164
13601
  const width = Math.max(...lines.map((line) => estimateDisplayTextWidth(line, s)));
12165
13602
  return { lines, width, column };
12166
13603
  };
@@ -12658,6 +14095,166 @@ function expandCoverBlock(config, context, template = "cover") {
12658
14095
  return coverBlock(input, context);
12659
14096
  }
12660
14097
 
14098
+ // src/doc/templates/markdownShapeGuide.ts
14099
+ var MARKDOWN_SHAPE_EXAMPLES = [
14100
+ {
14101
+ template: "process",
14102
+ cue: "a numbered list",
14103
+ brief: ["a numbered list", "steps"],
14104
+ drawn: "numbered steps joined by arrows",
14105
+ heading: "How a release ships",
14106
+ ordered: true,
14107
+ items: ["Draft the release notes", "Tag and test the build", "Roll out to everyone"]
14108
+ },
14109
+ {
14110
+ template: "cards",
14111
+ cue: "items written `Label: detail`",
14112
+ brief: ["`Label: detail` items", "cards"],
14113
+ drawn: "cards side by side",
14114
+ heading: "Three choices for next quarter",
14115
+ items: [
14116
+ "Fund: redesign permissions setup",
14117
+ "Protect: keep the activation gains",
14118
+ "Defer: advanced mobile editing"
14119
+ ]
14120
+ },
14121
+ {
14122
+ template: "stats",
14123
+ cue: "items written `Figure: what it measures`",
14124
+ brief: ["`Figure: what it measures` items", "key numbers"],
14125
+ drawn: "key numbers that count up",
14126
+ heading: "The quarter in numbers",
14127
+ items: ["18,420: active teams", "71%: weekly retained teams", "$3.8M: expansion revenue"]
14128
+ },
14129
+ {
14130
+ template: "process",
14131
+ cue: "items that open with dates in order",
14132
+ brief: ["items that open with dates", "a timeline"],
14133
+ drawn: "a timeline",
14134
+ heading: "Our story",
14135
+ items: [
14136
+ "2016: founded in a Portland garage",
14137
+ "2018: first product ships",
14138
+ "2022: Berlin office opens"
14139
+ ]
14140
+ },
14141
+ {
14142
+ template: "cycle",
14143
+ cue: "a heading that says cycle or loop",
14144
+ brief: ["a cycle or loop heading", "a ring"],
14145
+ drawn: "stages on a ring",
14146
+ heading: "The water cycle",
14147
+ items: [
14148
+ "Evaporation: the sun lifts water from the sea",
14149
+ "Condensation: vapour cools into cloud",
14150
+ "Precipitation: rain and snow fall"
14151
+ ]
14152
+ },
14153
+ {
14154
+ template: "layers",
14155
+ cue: "a heading that says layers or anatomy, with the innermost item first",
14156
+ brief: ["a layers or anatomy heading (innermost first)", "rings"],
14157
+ drawn: "concentric rings",
14158
+ heading: "Layers of the Earth",
14159
+ items: [
14160
+ "Inner core: solid iron",
14161
+ "Outer core: liquid iron",
14162
+ "Mantle: slowly flowing rock",
14163
+ "Crust: the thin outer layer"
14164
+ ]
14165
+ },
14166
+ {
14167
+ template: "pyramid",
14168
+ cue: "a heading that says hierarchy, pyramid, tiers or chain of command, with the top item first",
14169
+ brief: ["a hierarchy or pyramid heading (top first)", "a pyramid"],
14170
+ drawn: "a pyramid",
14171
+ heading: "The chain of command",
14172
+ items: [
14173
+ "Chief executive: sets the direction",
14174
+ "Vice presidents: turn it into plans",
14175
+ "Team leads: run the daily work",
14176
+ "Engineers: build the product"
14177
+ ]
14178
+ },
14179
+ {
14180
+ template: "funnel",
14181
+ cue: "a heading that says funnel or conversion, with the widest item first",
14182
+ brief: ["a funnel or conversion heading (widest first)", "a funnel"],
14183
+ drawn: "a funnel",
14184
+ heading: "The sign-up funnel",
14185
+ items: ["48,000: visit the site", "6,200: start a trial", "640: become customers"]
14186
+ },
14187
+ {
14188
+ template: "hub",
14189
+ cue: "a heading that names factors, causes, drivers or pillars over `Label: detail` items",
14190
+ brief: ["a factors, causes or drivers heading", "a hub"],
14191
+ drawn: "ideas around a hub",
14192
+ heading: "Three factors behind every delay",
14193
+ items: [
14194
+ "Scope: requirements keep changing",
14195
+ "Staffing: too few senior engineers",
14196
+ "Funding: released two months late"
14197
+ ],
14198
+ inputs: { center: "Delays" }
14199
+ },
14200
+ {
14201
+ template: "venn",
14202
+ cue: "two `Name: \u2026` items and a third `Both: \u2026`",
14203
+ brief: ["two items and a `Both: \u2026` item", "a Venn diagram"],
14204
+ drawn: "a Venn diagram",
14205
+ heading: "Designers and engineers",
14206
+ items: [
14207
+ "Designers: research and the interface",
14208
+ "Engineers: architecture and the code",
14209
+ "Both: accessibility and testing"
14210
+ ]
14211
+ },
14212
+ {
14213
+ template: "matrix",
14214
+ cue: "exactly four items under a heading that says matrix or quadrants (top left, top right, bottom left, bottom right)",
14215
+ brief: ["four items under a matrix or quadrants heading", "a two-by-two"],
14216
+ drawn: "a two-by-two",
14217
+ heading: "The effort and impact matrix",
14218
+ items: [
14219
+ "Quick wins: little effort, high impact",
14220
+ "Big bets: high effort, high impact",
14221
+ "Fill-ins: little effort, low impact",
14222
+ "Time sinks: high effort, low impact"
14223
+ ],
14224
+ inputs: { xAxis: "Effort", yAxis: "Impact" }
14225
+ },
14226
+ {
14227
+ template: "cards",
14228
+ cue: "two items under an `A vs B` heading",
14229
+ brief: ["two items under an `A vs B` heading", "two panels"],
14230
+ drawn: "two panels set against each other",
14231
+ heading: "Self-hosting vs a managed service",
14232
+ items: ["Self-hosting: costs less, needs an engineer", "Managed: costs more, includes backups"]
14233
+ }
14234
+ ];
14235
+ function markdownShapeExample(template, options = {}) {
14236
+ const example = MARKDOWN_SHAPE_EXAMPLES.find((entry) => entry.template === template);
14237
+ const hashes = "#".repeat(Math.min(6, Math.max(1, options.headingLevel ?? 2)));
14238
+ const inputs = Object.entries(example.inputs ?? {}).map(([key, value]) => ` ${key}="${value}"`).join("");
14239
+ return [
14240
+ `${hashes} ${example.heading} {[${template}${inputs}]}`,
14241
+ "",
14242
+ ...example.items.map((item, index) => `${example.ordered ? `${index + 1}.` : "-"} ${item}`)
14243
+ ].join("\n");
14244
+ }
14245
+ function markdownShapeGuide() {
14246
+ return [
14247
+ "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:",
14248
+ ...MARKDOWN_SHAPE_EXAMPLES.map((example) => `- ${example.cue} \u2192 ${example.drawn}`),
14249
+ `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.`,
14250
+ "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."
14251
+ ];
14252
+ }
14253
+ function markdownShapeSummary() {
14254
+ const rules = MARKDOWN_SHAPE_EXAMPLES.map(({ brief }) => `${brief[0]} \u2192 ${brief[1]}`).join("; ");
14255
+ 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]}\`).`;
14256
+ }
14257
+
12661
14258
  // src/doc/templates/index.ts
12662
14259
  function materializeScheduledBlock(sourceBlock, blockIndex, totalBlocks, theme, viewport, persistentLayers, expandedPersistentLayers, registry, failureMode, onDiagnostic, motion, mediaAspectRatios) {
12663
14260
  const materialized = materializeBlockLayersWithRuntime(
@@ -12684,7 +14281,11 @@ function materializeScheduledBlock(sourceBlock, blockIndex, totalBlocks, theme,
12684
14281
  audioSegment: sourceBlock.audioSegment,
12685
14282
  ...materialized.layers.length > 0 ? { layers: materialized.layers } : {},
12686
14283
  transition: sourceBlock.transition,
12687
- template: sourceBlock.template,
14284
+ // A block its template could not draw was drawn as content. Name what
14285
+ // drew it, so renderers size its text as a content slide's: they skip
14286
+ // grow-to-fit by template name, and "comparisonBar" would keep a
14287
+ // paragraph at chart-label size.
14288
+ template: materialized.diagnostic?.code === "template-drew-nothing" ? "content" : sourceBlock.template,
12688
14289
  build: sourceBlock.build,
12689
14290
  buildTiming: sourceBlock.buildTiming,
12690
14291
  buildSource: sourceBlock.buildSource,
@@ -12896,11 +14497,12 @@ function expandDocBlocks(blocks, options = {}) {
12896
14497
  }
12897
14498
  }
12898
14499
  const MIN_TRANSITION_GAP = 5;
14500
+ const GAP_TOLERANCE = 1e-3;
12899
14501
  const segmentEnd = audioSegment.startTime + audioSegment.duration;
12900
14502
  while (segmentExpandedBlocks.length > 1) {
12901
14503
  const lastBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 1];
12902
14504
  const timeFromLastToEnd = segmentEnd - lastBlock.startTime;
12903
- if (mergeShortBlocks && timeFromLastToEnd < MIN_TRANSITION_GAP && lastBlock.template !== "sectionHeader") {
14505
+ if (mergeShortBlocks && timeFromLastToEnd < MIN_TRANSITION_GAP - GAP_TOLERANCE && lastBlock.template !== "sectionHeader") {
12904
14506
  const prevBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 2];
12905
14507
  prevBlock.duration = segmentEnd - prevBlock.startTime;
12906
14508
  lastBlock.duration = 0;
@@ -12924,7 +14526,7 @@ function expandDocBlocks(blocks, options = {}) {
12924
14526
  changed = false;
12925
14527
  for (let i = segmentExpandedBlocks.length - 1; i >= 1; i--) {
12926
14528
  const block = segmentExpandedBlocks[i];
12927
- if (block.duration > 0 && block.duration < MIN_TRANSITION_GAP) {
14529
+ if (block.duration > 0 && block.duration < MIN_TRANSITION_GAP - GAP_TOLERANCE) {
12928
14530
  const prev = segmentExpandedBlocks[i - 1];
12929
14531
  const blockEnd = block.startTime + block.duration;
12930
14532
  prev.duration = blockEnd - prev.startTime;
@@ -12943,7 +14545,7 @@ function expandDocBlocks(blocks, options = {}) {
12943
14545
  if (block.duration > MAX_BLOCK_DURATION) {
12944
14546
  const numParts = Math.ceil(block.duration / MAX_BLOCK_DURATION);
12945
14547
  const partDuration = block.duration / numParts;
12946
- if (partDuration >= MIN_TRANSITION_GAP) {
14548
+ if (partDuration >= MIN_TRANSITION_GAP - GAP_TOLERANCE) {
12947
14549
  block.duration = partDuration;
12948
14550
  const parts = [];
12949
14551
  for (let p = 1; p < numParts; p++) {
@@ -13117,15 +14719,15 @@ var SLUG_NON_WORD_RE = /[^\w\s-]/g;
13117
14719
  var SLUG_SPACES_RE = /[\s_]+/g;
13118
14720
  var SLUG_MULTI_HYPHEN_RE = /-+/g;
13119
14721
  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
14722
+ function slugify(text2) {
14723
+ 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
14724
  "block";
13123
14725
  }
13124
14726
  function createIdGenerator() {
13125
14727
  const used = /* @__PURE__ */ new Map();
13126
14728
  const generate = (heading, _index) => {
13127
- const text = extractPlainText(heading);
13128
- const base = slugify(text);
14729
+ const text2 = extractPlainText(heading);
14730
+ const base = slugify(text2);
13129
14731
  const count = used.get(base) ?? 0;
13130
14732
  used.set(base, count + 1);
13131
14733
  if (count === 0) return base;
@@ -13134,8 +14736,8 @@ function createIdGenerator() {
13134
14736
  generate.reserve = (id) => {
13135
14737
  used.set(id, Math.max(used.get(id) ?? 0, 1));
13136
14738
  };
13137
- generate.fromText = (text) => {
13138
- const base = slugify(text);
14739
+ generate.fromText = (text2) => {
14740
+ const base = slugify(text2);
13139
14741
  const count = used.get(base) ?? 0;
13140
14742
  used.set(base, count + 1);
13141
14743
  return count === 0 ? base : `${base}-${count + 1}`;
@@ -13340,10 +14942,15 @@ function markdownToDoc(markdownDoc, options) {
13340
14942
  applyTimelineData(block, diagnostics);
13341
14943
  applyTreeData(block, diagnostics);
13342
14944
  }
13343
- if ((options?.autoTemplates ?? true) && !frontmatterDisablesAutoTemplates(markdownDoc)) {
13344
- applyAutoTemplates(rootBlocks, resolveTemplateName(defaultTemplate), { featureIndex: 0 });
14945
+ const autoLevel = frontmatterAutoTemplates(markdownDoc, options?.autoTemplates ?? true);
14946
+ if (autoLevel) {
14947
+ applyAutoTemplates(rootBlocks, resolveTemplateName(defaultTemplate), {
14948
+ featureIndex: 0,
14949
+ concepts: autoLevel !== "structural"
14950
+ });
13345
14951
  }
13346
14952
  for (const block of allBlocks) collectTimelineWarnings(block, diagnostics);
14953
+ for (const block of allBlocks) collectShapeWarning(block, diagnostics);
13347
14954
  const seenIds = /* @__PURE__ */ new Map();
13348
14955
  for (const block of allBlocks) {
13349
14956
  if (seenIds.has(block.id)) {
@@ -13561,6 +15168,23 @@ function applyTimelineData(block, diagnostics) {
13561
15168
  }
13562
15169
  block.templateData = { ...derived, ...block.templateData };
13563
15170
  }
15171
+ function collectShapeWarning(block, diagnostics) {
15172
+ if (!block.sourceHeading?.templateAnnotation?.template && !block.promotedBodyAnnotation) return;
15173
+ const template = conceptTemplate(resolveTemplateName(block.template ?? ""));
15174
+ if (!template) return;
15175
+ const given = block.templateData?.items;
15176
+ const items = Array.isArray(given) ? given : extractConceptItems(block.contents);
15177
+ if (!items.length) return;
15178
+ const downgrade = conceptDowngrade(template, items);
15179
+ if (!downgrade) return;
15180
+ diagnostics.push({
15181
+ severity: "warning",
15182
+ code: "template-shape-unmet",
15183
+ message: `Block "${block.id}" asks for ${template} but is drawn as ${downgrade.drawsAs === "list" ? "a list" : downgrade.drawsAs}: ${downgrade.reason}`,
15184
+ blockId: block.id,
15185
+ ...lineOf(block)
15186
+ });
15187
+ }
13564
15188
  function collectTimelineWarnings(block, diagnostics) {
13565
15189
  if (resolveTemplateName(block.template ?? "") !== "timeline") return;
13566
15190
  if (!block.templateData || !("tracks" in block.templateData)) return;
@@ -13578,20 +15202,26 @@ function collectTimelineWarnings(block, diagnostics) {
13578
15202
  });
13579
15203
  }
13580
15204
  }
13581
- function frontmatterDisablesAutoTemplates(markdownDoc) {
15205
+ function frontmatterAutoTemplates(markdownDoc, option) {
15206
+ if (!option) return false;
13582
15207
  const v = markdownDoc.frontmatter?.["squisq-auto-templates"];
13583
- return v === false || v === "false" || v === "off" || v === "no" || v === 0;
15208
+ if (v === false || v === "false" || v === "off" || v === "no" || v === 0) return false;
15209
+ if (v === "structural" || v === "basic") return "structural";
15210
+ return option;
13584
15211
  }
13585
15212
  function applyAutoTemplates(blocks, resolvedDefault, state) {
13586
15213
  for (const block of blocks) {
13587
15214
  const annotated = !!block.sourceHeading?.templateAnnotation?.template || !!block.promotedBodyAnnotation;
13588
15215
  if (block.sourceHeading && !annotated && block.template === resolvedDefault) {
13589
- const profile = profileBlockContents(block.contents ?? []);
13590
- const picked = pickAutoTemplate(profile, state.featureIndex);
15216
+ const profile = profileBlockContents(block.contents ?? [], { heading: block.title });
15217
+ let picked = pickAutoTemplate(profile, state.featureIndex, { concepts: state.concepts });
15218
+ const drawn = !!picked && picked !== "list" && profile.listShape?.template === picked;
15219
+ if (drawn && !autoTemplatePreservesContent(picked, block.contents)) picked = "list";
13591
15220
  if (picked && autoTemplatePreservesContent(picked, block.contents)) {
13592
- const inputs = deriveTemplateInputs(picked, block.title ?? "", block.contents, {
15221
+ const derived = deriveTemplateInputs(picked, block.title ?? "", block.contents, {
13593
15222
  preserveSourceHeading: true
13594
15223
  });
15224
+ const inputs = derived && drawn && picked !== "list" ? { ...derived, ...profile.listShape?.inputs } : derived;
13595
15225
  if (inputs) {
13596
15226
  if (picked === "leftFeature" || picked === "rightFeature") state.featureIndex += 1;
13597
15227
  block.template = picked;
@@ -13612,8 +15242,8 @@ function getBlockBodyText2(block) {
13612
15242
  return block.contents.filter((node) => !(consumedFence && node.type === "code")).filter((node) => !isMaterializedMediaNode(node)).map((node) => extractPlainText(node)).join("\n").trim();
13613
15243
  }
13614
15244
  var MAX_PHRASE_WORDS = 12;
13615
- function splitIntoSentences(text) {
13616
- const lines = text.split(/\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
15245
+ function splitIntoSentences(text2) {
15246
+ const lines = text2.split(/\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
13617
15247
  const sentenceRe = /(?<=[.!?])\s+/;
13618
15248
  const fragments = [];
13619
15249
  for (const line of lines) {
@@ -13785,12 +15415,12 @@ function buildNarrationScript(doc, options) {
13785
15415
  const separator = options?.blockSeparator ?? "\n\n";
13786
15416
  const pieces = [];
13787
15417
  for (const block of flattenRenderableBlocks(doc.blocks)) {
13788
- const text = blockSpokenText(block, includeTitles);
13789
- if (!text) continue;
15418
+ const text2 = blockSpokenText(block, includeTitles);
15419
+ if (!text2) continue;
13790
15420
  pieces.push({
13791
15421
  blockId: block.id,
13792
15422
  heading: block.title || void 0,
13793
- text
15423
+ text: text2
13794
15424
  });
13795
15425
  }
13796
15426
  let sourceText = "";
@@ -13831,20 +15461,20 @@ function buildNarrationScript(doc, options) {
13831
15461
  spoken
13832
15462
  });
13833
15463
  }
13834
- for (const range of blocks) {
13835
- range.tokenStart = tokens.length;
13836
- range.tokenEnd = 0;
15464
+ for (const range2 of blocks) {
15465
+ range2.tokenStart = tokens.length;
15466
+ range2.tokenEnd = 0;
13837
15467
  }
13838
15468
  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;
15469
+ const range2 = blocks[tokens[i].blockIndex];
15470
+ if (range2) {
15471
+ if (i < range2.tokenStart) range2.tokenStart = i;
15472
+ range2.tokenEnd = i + 1;
13843
15473
  }
13844
15474
  }
13845
- for (const range of blocks) {
13846
- if (range.tokenStart > range.tokenEnd) {
13847
- range.tokenStart = range.tokenEnd;
15475
+ for (const range2 of blocks) {
15476
+ if (range2.tokenStart > range2.tokenEnd) {
15477
+ range2.tokenStart = range2.tokenEnd;
13848
15478
  }
13849
15479
  }
13850
15480
  for (let i = 0; i < tokens.length; i++) {
@@ -13943,9 +15573,9 @@ function planContentBuilds(nodes, spec) {
13943
15573
  const steps = [];
13944
15574
  const targets = /* @__PURE__ */ new Map();
13945
15575
  if (!spec || spec.mode === "off") return { steps, targets };
13946
- const add = (node, text, ancestors) => {
15576
+ const add = (node, text2, ancestors) => {
13947
15577
  const index = steps.length;
13948
- steps.push({ id: `build-${index}`, text, ...ancestors.length ? { ancestors } : {} });
15578
+ steps.push({ id: `build-${index}`, text: text2, ...ancestors.length ? { ancestors } : {} });
13949
15579
  targets.set(node, index);
13950
15580
  return index;
13951
15581
  };
@@ -14067,26 +15697,26 @@ function resolveDocBuildTiming(doc, blocks) {
14067
15697
  });
14068
15698
  return { ...block, buildTiming: { sourceText: buildSourceText(steps), starts: starts2 } };
14069
15699
  }
14070
- const range = script?.blocks.find(
14071
- (range2) => range2.blockId === (block.sourceBlockId ?? block.id)
15700
+ const range2 = script?.blocks.find(
15701
+ (range3) => range3.blockId === (block.sourceBlockId ?? block.id)
14072
15702
  );
14073
- const original = range ? flattenRenderableBlocks(doc.blocks).find((source2) => source2.id === range.blockId) : void 0;
15703
+ const original = range2 ? flattenRenderableBlocks(doc.blocks).find((source2) => source2.id === range2.blockId) : void 0;
14074
15704
  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);
15705
+ const charStart = range2 ? body ? range2.charEnd - body.length : range2.charStart : 0;
15706
+ const source = block.buildSource ?? (range2 ? { text: narration.sourceText.slice(charStart, range2.charEnd), charStart } : void 0);
14077
15707
  if (!source) return block;
14078
15708
  let cursor = 0;
14079
15709
  const starts = [];
14080
15710
  for (const step2 of steps) {
14081
- const text = step2.text.trim();
14082
- const offset = text ? source.text.indexOf(text, cursor) : -1;
15711
+ const text2 = step2.text.trim();
15712
+ const offset = text2 ? source.text.indexOf(text2, cursor) : -1;
14083
15713
  if (offset < 0) return block;
14084
15714
  const word = narration.bookmarks.find(
14085
- (word2) => word2.charOffset >= source.charStart + offset && word2.charOffset < source.charStart + offset + text.length
15715
+ (word2) => word2.charOffset >= source.charStart + offset && word2.charOffset < source.charStart + offset + text2.length
14086
15716
  );
14087
15717
  if (!word) return block;
14088
15718
  starts.push(Math.max(0, word.time - block.startTime));
14089
- cursor = offset + text.length;
15719
+ cursor = offset + text2.length;
14090
15720
  }
14091
15721
  return { ...block, buildTiming: { sourceText: buildSourceText(steps), starts } };
14092
15722
  });
@@ -14121,10 +15751,10 @@ function listParagraphs(list, level = 0) {
14121
15751
  return paragraphs;
14122
15752
  });
14123
15753
  }
14124
- function plainParagraphText({ text, list }) {
14125
- if (!list) return text;
15754
+ function plainParagraphText({ text: text2, list }) {
15755
+ if (!list) return text2;
14126
15756
  const marker = list.ordered ? `${list.number ?? 1}.` : "\u2022";
14127
- return `${" ".repeat(list.level)}${marker} ${text}`;
15757
+ return `${" ".repeat(list.level)}${marker} ${text2}`;
14128
15758
  }
14129
15759
  function completeBodyParagraphs(nodes) {
14130
15760
  const paragraphs = nodes.flatMap((node, index) => {
@@ -14245,7 +15875,15 @@ export {
14245
15875
  countUpSpecFor,
14246
15876
  formatCountValue,
14247
15877
  formatCountUp,
14248
- splitConceptItem,
15878
+ listBlock,
15879
+ CONCEPT_TEMPLATES,
15880
+ CONCEPT_MAX_DRAWN,
15881
+ CONCEPT_SPECS,
15882
+ conceptTemplate,
15883
+ conceptItemCount,
15884
+ conceptItemsProblem,
15885
+ conceptDowngrade,
15886
+ conceptDrawsAs,
14249
15887
  conceptBuildStepTexts,
14250
15888
  cardsBlock,
14251
15889
  processBlock,
@@ -14264,7 +15902,12 @@ export {
14264
15902
  mediaOnly,
14265
15903
  mapBlock,
14266
15904
  fullBleedQuote,
14267
- listBlock,
15905
+ statsBlock,
15906
+ pyramidBlock,
15907
+ funnelBlock,
15908
+ hubBlock,
15909
+ vennBlock,
15910
+ matrixBlock,
14268
15911
  photoGrid,
14269
15912
  definitionCard,
14270
15913
  comparisonBar,
@@ -14314,14 +15957,30 @@ export {
14314
15957
  materializeBlockLayersWithRuntime,
14315
15958
  TEMPLATE_METADATA,
14316
15959
  TEMPLATE_AUTHORING_METADATA,
15960
+ PRESENTATION_KEY,
15961
+ PRESENTATION_LAYOUTS,
15962
+ AUTHORED_PRESENTATION_LAYOUT,
15963
+ presentationStat,
15964
+ presentationLayoutFits,
15965
+ presentationLayoutProblem,
15966
+ presentationLayoutNeeds,
15967
+ presentationBeatExtras,
15968
+ PRESENTATION_LIMITS,
15969
+ parsePresentationPlan,
15970
+ serializePresentationPlan,
14317
15971
  TEMPLATE_GUIDES,
14318
15972
  guideText,
14319
15973
  templateCatalog,
14320
15974
  PRESENTATION_LAYOUT_GUIDE,
15975
+ PRESENTATION_LAYOUT_LABELS,
14321
15976
  AUTHORING_TRICKS,
14322
15977
  authoringTricks,
14323
15978
  coverBlock,
14324
15979
  expandCoverBlock,
15980
+ MARKDOWN_SHAPE_EXAMPLES,
15981
+ markdownShapeExample,
15982
+ markdownShapeGuide,
15983
+ markdownShapeSummary,
14325
15984
  expandDocBlocks,
14326
15985
  getAvailableTemplates,
14327
15986
  hasTemplate,