@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.
- package/dist/{Doc-BhvotZx_.d.ts → Doc-C_Sh49ot.d.ts} +59 -6
- package/dist/{ImageEditDoc-D8hYio6b.d.ts → ImageEditDoc-Sqw3Tw-R.d.ts} +1 -1
- package/dist/{chunk-OIQ2GO2O.js → chunk-2CB37BOU.js} +1 -1
- package/dist/{chunk-CTT4XAGO.js → chunk-4HQ7QZKB.js} +2289 -630
- package/dist/chunk-C7DKMO4H.js +88 -0
- package/dist/{chunk-C4SB3NN4.js → chunk-DDJHIZED.js} +1 -1
- package/dist/{chunk-TCGGJUKC.js → chunk-FYP7GNER.js} +93 -15
- package/dist/chunk-N3FQSUVT.js +515 -0
- package/dist/{chunk-SGNWO7OF.js → chunk-OFE5RZWW.js} +490 -24
- package/dist/{chunk-YQQWE4HG.js → chunk-OJCAKUHW.js} +192 -260
- package/dist/{chunk-7MS26OXD.js → chunk-UDPPWYYM.js} +6 -0
- package/dist/{chunk-5DFFVW75.js → chunk-Y34KYMAS.js} +1 -1
- package/dist/doc/index.d.ts +337 -22
- package/dist/doc/index.js +56 -8
- package/dist/fence/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/imageEdit/index.d.ts +3 -3
- package/dist/index.d.ts +8 -8
- package/dist/index.js +127 -21
- package/dist/jsonForm/index.d.ts +1 -1
- package/dist/markdown/index.js +3 -3
- package/dist/{materializePageSection-CWxou4mX.d.ts → materializePageSection-Dmf-rf1b.d.ts} +121 -8
- package/dist/mediaEdit/index.d.ts +2 -2
- package/dist/narration/index.d.ts +1 -1
- package/dist/narration/index.js +4 -4
- package/dist/recommend/index.d.ts +129 -2
- package/dist/recommend/index.js +11 -3
- package/dist/schemas/index.d.ts +4 -4
- package/dist/{themeLibrary-BIYE6aLw.d.ts → themeLibrary-B4VLovhW.d.ts} +1 -1
- package/dist/timing/index.d.ts +66 -2
- package/dist/timing/index.js +12 -0
- package/dist/transform/index.d.ts +173 -16
- package/dist/transform/index.js +55 -16
- package/package.json +1 -1
- 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
|
-
|
|
35
|
-
|
|
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-
|
|
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
|
|
633
|
-
if (
|
|
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(
|
|
726
|
-
return
|
|
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(
|
|
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(
|
|
747
|
+
const lines = Math.max(1, Math.ceil(text2.length / charsPerLine));
|
|
740
748
|
return lines * fontSizePx * lineHeight;
|
|
741
749
|
}
|
|
742
|
-
function estimateWrappedLineCount(
|
|
743
|
-
if (!
|
|
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
|
|
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(
|
|
765
|
-
return
|
|
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(
|
|
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(
|
|
1296
|
-
return
|
|
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(
|
|
1330
|
+
function estimateDisplayTextWidth(text2, fontSize) {
|
|
1323
1331
|
let em = 0;
|
|
1324
|
-
for (const ch of
|
|
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
|
|
1662
|
-
if (
|
|
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(
|
|
1679
|
-
return
|
|
1686
|
+
function escapeBraces(text2) {
|
|
1687
|
+
return text2.replace(ESCAPE_RE, (_match, inner) => ESCAPED_OPEN + inner + ESCAPED_CLOSE);
|
|
1680
1688
|
}
|
|
1681
|
-
function unescapeBraces(
|
|
1682
|
-
return
|
|
1689
|
+
function unescapeBraces(text2) {
|
|
1690
|
+
return text2.replace(new RegExp(ESCAPED_OPEN, "g"), "{").replace(new RegExp(ESCAPED_CLOSE, "g"), "}");
|
|
1683
1691
|
}
|
|
1684
|
-
function substitute(
|
|
1685
|
-
const escaped = escapeBraces(
|
|
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(
|
|
1729
|
-
return substitute(
|
|
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((
|
|
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
|
-
|
|
1877
|
-
|
|
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(
|
|
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(
|
|
1890
|
-
const parsed = parseStatNumber(
|
|
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(
|
|
1913
|
-
const count = animation.count ?? countUpSpecFor(
|
|
1914
|
-
if (!count) return
|
|
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) :
|
|
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/
|
|
1928
|
-
var
|
|
1929
|
-
var
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
var
|
|
1939
|
-
var
|
|
1940
|
-
var
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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
|
|
1957
|
-
|
|
1958
|
-
const
|
|
1959
|
-
|
|
1960
|
-
|
|
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
|
|
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
|
|
1979
|
-
const {
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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: "
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
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
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
const
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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(
|
|
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
|
|
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(
|
|
4652
|
+
function fitWrapWidth(text2, fontSize, columnWidthPx, fontFamily) {
|
|
4272
4653
|
const mono = isMonoFamily(fontFamily);
|
|
4273
|
-
if (!mono && !WIDE_GLYPH_RE.test(
|
|
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(
|
|
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(
|
|
4546
|
-
const 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(
|
|
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/
|
|
4622
|
-
|
|
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
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
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
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
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
|
|
4656
|
-
const
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
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
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
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
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
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
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
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
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
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
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
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
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
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
|
|
4757
|
-
const
|
|
4758
|
-
const
|
|
4759
|
-
const
|
|
4760
|
-
const
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
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: "
|
|
5407
|
+
id: "hub-center",
|
|
5408
|
+
position: { x: cx, y: cy, width: side, height: side, anchor: "center" },
|
|
4770
5409
|
content: {
|
|
4771
|
-
text:
|
|
5410
|
+
text: center,
|
|
4772
5411
|
style: {
|
|
4773
|
-
fontSize:
|
|
5412
|
+
fontSize: fit.fontSize,
|
|
4774
5413
|
fontFamily: getThemeFont(context, "title"),
|
|
4775
5414
|
fontWeight: "bold",
|
|
4776
|
-
color:
|
|
4777
|
-
textAlign: "
|
|
4778
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
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
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
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
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
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
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
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
|
|
5767
|
-
if (!
|
|
6724
|
+
let text2 = raw.trim();
|
|
6725
|
+
if (!text2) return null;
|
|
5768
6726
|
let negative = false;
|
|
5769
|
-
const accounting = /^\((.*)\)$/.exec(
|
|
6727
|
+
const accounting = /^\((.*)\)$/.exec(text2);
|
|
5770
6728
|
if (accounting) {
|
|
5771
6729
|
negative = true;
|
|
5772
|
-
|
|
6730
|
+
text2 = accounting[1].trim();
|
|
5773
6731
|
}
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
if (/^[+-]?\d{1,3}(?: \d{3})+(?:\.\d+)?$/.test(
|
|
5778
|
-
|
|
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(
|
|
5781
|
-
const value = Number(
|
|
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
|
|
5798
|
-
if (!
|
|
5799
|
-
const lower =
|
|
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(
|
|
5803
|
-
const index = Number(
|
|
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(
|
|
5852
|
-
const exponent = Math.floor(Math.log10(
|
|
5853
|
-
const fraction =
|
|
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
|
|
5880
|
-
const step2 = niceNum(
|
|
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
|
|
6851
|
+
let text2;
|
|
5894
6852
|
if (abs >= 1e6) {
|
|
5895
|
-
|
|
6853
|
+
text2 = `${sign}${trimTrailingZero((abs / 1e6).toFixed(1))}M`;
|
|
5896
6854
|
} else if (abs >= 1e3) {
|
|
5897
|
-
|
|
6855
|
+
text2 = `${sign}${trimTrailingZero((abs / 1e3).toFixed(abs >= 1e4 ? 0 : 1))}K`;
|
|
5898
6856
|
} else {
|
|
5899
|
-
|
|
6857
|
+
text2 = `${sign}${abs.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
5900
6858
|
}
|
|
5901
|
-
return unit ? `${
|
|
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
|
|
5907
|
-
return unit ? `${
|
|
6864
|
+
const text2 = `${sign}${Math.abs(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
6865
|
+
return unit ? `${text2} ${unit}` : text2;
|
|
5908
6866
|
}
|
|
5909
|
-
function trimTrailingZero(
|
|
5910
|
-
return
|
|
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(
|
|
5948
|
-
if (
|
|
5949
|
-
return
|
|
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(
|
|
5952
|
-
if (
|
|
5953
|
-
const open = polylinePath(
|
|
5954
|
-
const first =
|
|
5955
|
-
const last =
|
|
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(
|
|
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 *
|
|
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 =
|
|
7254
|
-
const height =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
7975
|
-
return
|
|
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(
|
|
7978
|
-
if (!
|
|
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
|
|
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
|
|
8079
|
-
const innerWidth = Math.max(1,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
8221
|
-
(n) => typeof n === "object" && n !== null && typeof n.id === "string" && n.id.length > 0 && typeof n.label === "string"
|
|
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
|
|
8588
|
-
const id = typeof
|
|
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(
|
|
8591
|
-
|
|
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
|
|
8616
|
-
...typeof
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
10730
|
-
if (
|
|
10731
|
-
return `${
|
|
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(
|
|
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:
|
|
11258
|
-
y:
|
|
11259
|
-
width: Math.max(0,
|
|
11260
|
-
height: Math.max(0,
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
13390
|
+
fields: conceptFields("cycle"),
|
|
12033
13391
|
use: "stages that repeat in a loop, drawn on a ring"
|
|
12034
13392
|
},
|
|
12035
13393
|
layers: {
|
|
12036
|
-
fields:
|
|
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(
|
|
12041
|
-
return
|
|
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: "
|
|
12063
|
-
process: "
|
|
12064
|
-
cycle: "
|
|
12065
|
-
layers: "
|
|
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
|
|
12068
|
-
var
|
|
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 ${
|
|
12096
|
-
|
|
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
|
|
12130
|
-
return
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
13121
|
-
return
|
|
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
|
|
13128
|
-
const base = slugify(
|
|
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 = (
|
|
13138
|
-
const base = slugify(
|
|
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
|
-
|
|
13344
|
-
|
|
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
|
|
15205
|
+
function frontmatterAutoTemplates(markdownDoc, option) {
|
|
15206
|
+
if (!option) return false;
|
|
13582
15207
|
const v = markdownDoc.frontmatter?.["squisq-auto-templates"];
|
|
13583
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
13616
|
-
const lines =
|
|
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
|
|
13789
|
-
if (!
|
|
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
|
|
13835
|
-
|
|
13836
|
-
|
|
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
|
|
13840
|
-
if (
|
|
13841
|
-
if (i <
|
|
13842
|
-
|
|
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
|
|
13846
|
-
if (
|
|
13847
|
-
|
|
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,
|
|
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
|
|
14071
|
-
(
|
|
15700
|
+
const range2 = script?.blocks.find(
|
|
15701
|
+
(range3) => range3.blockId === (block.sourceBlockId ?? block.id)
|
|
14072
15702
|
);
|
|
14073
|
-
const original =
|
|
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 =
|
|
14076
|
-
const source = block.buildSource ?? (
|
|
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
|
|
14082
|
-
const offset =
|
|
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 +
|
|
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 +
|
|
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
|
|
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} ${
|
|
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
|
-
|
|
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
|
-
|
|
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,
|