@bendyline/squisq 2.13.11 → 2.13.13
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-DqYt_b2b.d.ts → Doc-BhvotZx_.d.ts} +48 -2
- package/dist/{ImageEditDoc-CXXiXVJ-.d.ts → ImageEditDoc-D8hYio6b.d.ts} +1 -1
- package/dist/{chunk-S76W65BB.js → chunk-5DFFVW75.js} +1 -1
- package/dist/{chunk-DOOUVVOM.js → chunk-7MS26OXD.js} +4 -0
- package/dist/{chunk-XDOSE5M3.js → chunk-C4SB3NN4.js} +1 -1
- package/dist/{chunk-SKGMNBQX.js → chunk-CTT4XAGO.js} +2033 -375
- package/dist/{chunk-SG3HXPMF.js → chunk-DBVCSVNV.js} +2 -1
- package/dist/chunk-FCB6JLWB.js +125 -0
- package/dist/{chunk-WNEH3BZP.js → chunk-JNUNNQ74.js} +3 -3
- package/dist/chunk-L4AFSOT3.js +26 -0
- package/dist/{chunk-65WL5ILB.js → chunk-OIQ2GO2O.js} +1 -1
- package/dist/{chunk-3WUI2N6C.js → chunk-SGNWO7OF.js} +20 -1
- package/dist/{chunk-ESGKI6CD.js → chunk-TCGGJUKC.js} +25 -6
- package/dist/{chunk-GLWQTZ4C.js → chunk-XEQALK5B.js} +1 -1
- package/dist/{chunk-N7I6JP6Q.js → chunk-YQQWE4HG.js} +413 -216
- package/dist/doc/index.d.ts +225 -9
- package/dist/doc/index.js +32 -7
- 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 +7 -7
- package/dist/index.js +79 -11
- package/dist/jsonForm/index.d.ts +1 -1
- package/dist/jsonForm/index.js +3 -3
- package/dist/markdown/index.js +3 -3
- package/dist/{materializePageSection-S2hxZ_lO.d.ts → materializePageSection-CWxou4mX.d.ts} +62 -2
- package/dist/mediaEdit/index.d.ts +2 -2
- package/dist/narration/index.d.ts +1 -1
- package/dist/narration/index.js +5 -5
- package/dist/recommend/index.js +1 -1
- package/dist/schemas/index.d.ts +4 -4
- package/dist/schemas/index.js +2 -2
- package/dist/{themeLibrary-CFJbeItn.d.ts → themeLibrary-BIYE6aLw.d.ts} +1 -1
- package/dist/timing/index.d.ts +58 -2
- package/dist/timing/index.js +9 -1
- package/dist/transform/index.d.ts +173 -72
- package/dist/transform/index.js +43 -6
- package/package.json +1 -1
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
profileBlockContents,
|
|
33
33
|
renderMarkdownBlocksHtml,
|
|
34
34
|
treeToTemplateData
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-SGNWO7OF.js";
|
|
36
36
|
import {
|
|
37
37
|
anchorPoint,
|
|
38
38
|
base64ToUtf8,
|
|
@@ -62,12 +62,12 @@ import {
|
|
|
62
62
|
validateCustomTemplateDefinition,
|
|
63
63
|
validateTheme,
|
|
64
64
|
withAlpha
|
|
65
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-JNUNNQ74.js";
|
|
66
66
|
import {
|
|
67
67
|
TABLE_FED_TEMPLATES,
|
|
68
68
|
isContainerTemplate,
|
|
69
69
|
resolveTemplateName
|
|
70
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-7MS26OXD.js";
|
|
71
71
|
import {
|
|
72
72
|
KNOWN_BLOCK_META_KEYS,
|
|
73
73
|
coerceAnnotationValues,
|
|
@@ -766,11 +766,11 @@ function estimateProseLineCount(text, fontSize, maxWidthPx) {
|
|
|
766
766
|
}
|
|
767
767
|
function fitProse(options) {
|
|
768
768
|
const { text, baseFontSize, minFontSize, maxWidthPx, maxHeightPx, lineHeight } = options;
|
|
769
|
-
const
|
|
769
|
+
const step2 = Math.max(0.5, options.step ?? 2);
|
|
770
770
|
const floor = Math.min(baseFontSize, minFontSize);
|
|
771
771
|
const heightAt = (lines, size) => lines * size * lineHeight;
|
|
772
772
|
const candidates = [];
|
|
773
|
-
for (let size = baseFontSize; size > floor; size -=
|
|
773
|
+
for (let size = baseFontSize; size > floor; size -= step2) candidates.push(size);
|
|
774
774
|
candidates.push(floor);
|
|
775
775
|
for (const size of candidates) {
|
|
776
776
|
const lines = estimateProseLineCount(text, size, maxWidthPx);
|
|
@@ -1924,6 +1924,1239 @@ function formatFinal(count) {
|
|
|
1924
1924
|
return `${count.prefix ?? ""}${formatCountValue(count.to, count.decimals ?? 0, count.grouping ?? false)}${count.suffix ?? ""}`;
|
|
1925
1925
|
}
|
|
1926
1926
|
|
|
1927
|
+
// src/doc/templates/conceptBlocks.ts
|
|
1928
|
+
var MAX_ITEMS = 6;
|
|
1929
|
+
var LABEL_WORDS = 7;
|
|
1930
|
+
function splitConceptItem(text) {
|
|
1931
|
+
const clean = text.replace(/\s+/gu, " ").trim();
|
|
1932
|
+
const match = clean.match(/^(.{1,64}?)(:\s|\s[—–-]\s)(.+)$/u);
|
|
1933
|
+
const range = !!match && !match[2].startsWith(":") && /\d$/u.test(match[1]);
|
|
1934
|
+
if (match && !range && !/[.!?]\s/u.test(match[1]) && match[1].split(" ").length <= LABEL_WORDS)
|
|
1935
|
+
return { label: match[1].trim(), detail: match[3].trim() };
|
|
1936
|
+
return { label: "", detail: clean };
|
|
1937
|
+
}
|
|
1938
|
+
var HEADLINE_BASE = 60;
|
|
1939
|
+
var HEADLINE_MIN = 40;
|
|
1940
|
+
var HEADLINE_LINE_HEIGHT = 1.15;
|
|
1941
|
+
var LABEL_BASE = 44;
|
|
1942
|
+
var DETAIL_BASE = 33;
|
|
1943
|
+
var PLAIN_BASE = 38;
|
|
1944
|
+
var LABEL_LINE_HEIGHT = 1.2;
|
|
1945
|
+
var DETAIL_LINE_HEIGHT = 1.4;
|
|
1946
|
+
var MIN_SCALE = 0.62;
|
|
1947
|
+
var MAX_SCALE = 1.18;
|
|
1948
|
+
var GROW_SHARE = 0.9;
|
|
1949
|
+
function ramp(context, scale) {
|
|
1950
|
+
return {
|
|
1951
|
+
label: Math.round(themedFontSize(LABEL_BASE, context, false) * scale),
|
|
1952
|
+
detail: Math.round(themedFontSize(DETAIL_BASE, context, false) * scale),
|
|
1953
|
+
plain: Math.round(themedFontSize(PLAIN_BASE, context, false) * scale)
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
function measureItem(item, sizes, width) {
|
|
1957
|
+
const labelH = item.label ? estimateProseLineCount(item.label, sizes.label, width) * sizes.label * LABEL_LINE_HEIGHT : 0;
|
|
1958
|
+
const size = item.label ? sizes.detail : sizes.plain;
|
|
1959
|
+
const detailH = item.detail ? estimateProseLineCount(item.detail, size, width) * size * DETAIL_LINE_HEIGHT : 0;
|
|
1960
|
+
const gap = item.label && item.detail ? sizes.label * 0.35 : 0;
|
|
1961
|
+
const widest = (text, size2, em) => Math.max(0, ...text.split(/\s+/u).map((word) => word.length)) * size2 * em;
|
|
1962
|
+
const overflows = widest(item.label, sizes.label, 0.62) > width || widest(item.detail, size, 0.56) > width;
|
|
1963
|
+
return { labelH, gap, detailH, height: overflows ? Infinity : labelH + gap + detailH };
|
|
1964
|
+
}
|
|
1965
|
+
var SCALES = [MAX_SCALE, 1, 0.82, MIN_SCALE];
|
|
1966
|
+
function fitRamp(items, context, width, height) {
|
|
1967
|
+
for (const scale of SCALES) {
|
|
1968
|
+
const sizes = ramp(context, scale);
|
|
1969
|
+
const room = height(scale) * (scale > 1 ? GROW_SHARE : 1);
|
|
1970
|
+
if (items.every((item) => measureItem(item, sizes, width(scale)).height <= room))
|
|
1971
|
+
return { sizes, scale };
|
|
1972
|
+
}
|
|
1973
|
+
return { sizes: ramp(context, MIN_SCALE), scale: MIN_SCALE };
|
|
1974
|
+
}
|
|
1975
|
+
function accentColor(input, context) {
|
|
1976
|
+
return input.colorScheme ? resolveColorScheme(context, input.colorScheme).accent : context.theme.colors.highlight || context.theme.colors.primary;
|
|
1977
|
+
}
|
|
1978
|
+
function frame(input, context) {
|
|
1979
|
+
const { theme, viewport } = context;
|
|
1980
|
+
const { width: W, height: H } = viewport;
|
|
1981
|
+
const portrait = H > W;
|
|
1982
|
+
const accent = accentColor(input, context);
|
|
1983
|
+
const margin = Math.round(W * (portrait ? 0.08 : 0.07));
|
|
1984
|
+
const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
|
|
1985
|
+
const title = (input.title ?? "").trim();
|
|
1986
|
+
let top = H * 0.1;
|
|
1987
|
+
if (title) {
|
|
1988
|
+
const headlineTop = H * (portrait ? 0.08 : 0.11);
|
|
1989
|
+
const base = themedFontSize(HEADLINE_BASE, context, true);
|
|
1990
|
+
const fit = fitProse({
|
|
1991
|
+
text: title,
|
|
1992
|
+
baseFontSize: base,
|
|
1993
|
+
minFontSize: Math.min(base, themedFontSize(HEADLINE_MIN, context, true)),
|
|
1994
|
+
maxWidthPx: W - margin * 2,
|
|
1995
|
+
maxHeightPx: base * HEADLINE_LINE_HEIGHT * 2,
|
|
1996
|
+
lineHeight: HEADLINE_LINE_HEIGHT
|
|
1997
|
+
});
|
|
1998
|
+
const ruleH = Math.max(4, Math.round(H * 6e-3));
|
|
1999
|
+
layers.push({
|
|
2000
|
+
type: "shape",
|
|
2001
|
+
id: "accent-rule",
|
|
2002
|
+
position: {
|
|
2003
|
+
x: margin,
|
|
2004
|
+
y: headlineTop - ruleH - fit.fontSize * 0.55,
|
|
2005
|
+
width: W * 0.045,
|
|
2006
|
+
height: ruleH
|
|
2007
|
+
},
|
|
2008
|
+
content: { shape: "rect", fill: accent, borderRadius: ruleH / 2 },
|
|
2009
|
+
animation: entrance(context, "rule", 0)
|
|
2010
|
+
});
|
|
2011
|
+
layers.push({
|
|
2012
|
+
...textLayer("title", title, box(margin, headlineTop, W - margin * 2, fit.heightPx), {
|
|
2013
|
+
fontSize: fit.fontSize,
|
|
2014
|
+
fontFamily: getThemeFont(context, "title"),
|
|
2015
|
+
fontWeight: "bold",
|
|
2016
|
+
color: theme.colors.text,
|
|
2017
|
+
lineHeight: HEADLINE_LINE_HEIGHT,
|
|
2018
|
+
shadow: shouldUseShadow(context),
|
|
2019
|
+
...fit.maxLines ? { maxLines: fit.maxLines } : {}
|
|
2020
|
+
}),
|
|
2021
|
+
animation: entrance(context, "text", 0.1)
|
|
2022
|
+
});
|
|
2023
|
+
top = headlineTop + fit.heightPx + H * (portrait ? 0.05 : 0.07);
|
|
2024
|
+
}
|
|
2025
|
+
return {
|
|
2026
|
+
layers,
|
|
2027
|
+
accent,
|
|
2028
|
+
left: margin,
|
|
2029
|
+
right: W - margin,
|
|
2030
|
+
top,
|
|
2031
|
+
bottom: H * (portrait ? 0.92 : 0.9),
|
|
2032
|
+
portrait
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
function box(left, top, width, height) {
|
|
2036
|
+
return { x: left + width / 2, y: top + height / 2, width, height, anchor: "center" };
|
|
2037
|
+
}
|
|
2038
|
+
function textLayer(id, text, position, style) {
|
|
2039
|
+
return {
|
|
2040
|
+
type: "text",
|
|
2041
|
+
id,
|
|
2042
|
+
position,
|
|
2043
|
+
content: {
|
|
2044
|
+
text,
|
|
2045
|
+
style: { textAlign: "left", verticalAlign: "top", shrinkToFit: true, ...style }
|
|
2046
|
+
}
|
|
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;
|
|
2071
|
+
layers.push(
|
|
2072
|
+
textLayer(
|
|
2073
|
+
`${id}-detail`,
|
|
2074
|
+
item.detail,
|
|
2075
|
+
box(left, top + measure.labelH + measure.gap, width, measure.detailH + slack(size)),
|
|
2076
|
+
{
|
|
2077
|
+
fontSize: size,
|
|
2078
|
+
fontFamily: getThemeFont(context, "body"),
|
|
2079
|
+
// Details read as secondary by weight and a little transparency, not a
|
|
2080
|
+
// muted hue that loses contrast on dark themes.
|
|
2081
|
+
color: item.label ? withAlpha(theme.colors.text, 0.8) : theme.colors.text,
|
|
2082
|
+
lineHeight: DETAIL_LINE_HEIGHT,
|
|
2083
|
+
textAlign: align
|
|
2084
|
+
}
|
|
2085
|
+
)
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
return layers;
|
|
2089
|
+
}
|
|
2090
|
+
function itemsOf(input) {
|
|
2091
|
+
return (Array.isArray(input.items) ? input.items : []).map((item) => String(item ?? "").trim()).filter(Boolean).slice(0, MAX_ITEMS).map(splitConceptItem);
|
|
2092
|
+
}
|
|
2093
|
+
function conceptBuildStepTexts(template, items) {
|
|
2094
|
+
const shown = itemsOf({ items: [...items] });
|
|
2095
|
+
const dated = template === "process" ? timelineItems(shown) : void 0;
|
|
2096
|
+
if (dated)
|
|
2097
|
+
return dated.map((item) => [item.when, item.label, item.detail].filter(Boolean).join(" "));
|
|
2098
|
+
return shown.map(
|
|
2099
|
+
(item, index) => [
|
|
2100
|
+
template === "cards" || template === "layers" ? "" : String(index + 1),
|
|
2101
|
+
item.label,
|
|
2102
|
+
item.detail
|
|
2103
|
+
].filter(Boolean).join(" ")
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
var FIRST_TURN = 0.6;
|
|
2107
|
+
var TURN_STAGGER = 0.45;
|
|
2108
|
+
var SETTLE = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
2109
|
+
function entrance(context, kind, delay) {
|
|
2110
|
+
const profile = getMotionProfile(context);
|
|
2111
|
+
if (kind === "text" || !profile.diagramBuild)
|
|
2112
|
+
return motionEntrance(context, { type: "fadeIn", duration: 0.7, delay });
|
|
2113
|
+
const at = motionDelay(context, delay);
|
|
2114
|
+
switch (kind) {
|
|
2115
|
+
case "words":
|
|
2116
|
+
return { type: "wordReveal", duration: 0.5, stagger: 0.07, delay: at, easing: SETTLE };
|
|
2117
|
+
case "surface":
|
|
2118
|
+
return { type: "fadeInUp", duration: 0.6, delay: at, easing: SETTLE };
|
|
2119
|
+
case "rule":
|
|
2120
|
+
return { type: "grow", origin: "left", duration: 0.55, delay: at, easing: SETTLE };
|
|
2121
|
+
case "bar":
|
|
2122
|
+
return { type: "grow", origin: "top", duration: 0.6, delay: at, easing: SETTLE };
|
|
2123
|
+
case "pop":
|
|
2124
|
+
return {
|
|
2125
|
+
type: "grow",
|
|
2126
|
+
origin: "center",
|
|
2127
|
+
duration: 0.5,
|
|
2128
|
+
delay: at,
|
|
2129
|
+
easing: profile.name === "vibrant" ? "cubic-bezier(0.34, 1.56, 0.64, 1)" : "cubic-bezier(0.34, 1.3, 0.64, 1)"
|
|
2130
|
+
};
|
|
2131
|
+
case "draw":
|
|
2132
|
+
return { type: "drawOn", duration: 0.5, delay: at, easing: "ease-in-out" };
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
function step(parts, index, context) {
|
|
2136
|
+
const turn = FIRST_TURN + index * TURN_STAGGER;
|
|
2137
|
+
return parts.map(([layer, kind, at]) => ({
|
|
2138
|
+
...layer,
|
|
2139
|
+
buildStep: index,
|
|
2140
|
+
animation: entrance(context, kind, turn + at)
|
|
2141
|
+
}));
|
|
2142
|
+
}
|
|
2143
|
+
function pops(layers, at) {
|
|
2144
|
+
return layers.map((layer) => [layer, "pop", at]);
|
|
2145
|
+
}
|
|
2146
|
+
function words(layers, at) {
|
|
2147
|
+
return layers.map((layer, i) => [layer, "text", at + i * 0.12]);
|
|
2148
|
+
}
|
|
2149
|
+
var LEAD_BASE = 44;
|
|
2150
|
+
var LEAD_MIN = 28;
|
|
2151
|
+
var LEAD_LINE_HEIGHT = 1.4;
|
|
2152
|
+
function lead(area, text, context) {
|
|
2153
|
+
const { theme, viewport } = context;
|
|
2154
|
+
const barW = Math.max(5, Math.round(viewport.width * 4e-3));
|
|
2155
|
+
const indent = barW + Math.round(viewport.width * 0.018);
|
|
2156
|
+
const width = (area.right - area.left) * (area.portrait ? 1 : 0.8) - indent;
|
|
2157
|
+
const base = themedFontSize(
|
|
2158
|
+
text.length <= 90 ? LEAD_BASE * 1.3 : text.length <= 160 ? LEAD_BASE * 1.15 : LEAD_BASE,
|
|
2159
|
+
context,
|
|
2160
|
+
false
|
|
2161
|
+
);
|
|
2162
|
+
const fit = fitProse({
|
|
2163
|
+
text,
|
|
2164
|
+
baseFontSize: base,
|
|
2165
|
+
minFontSize: Math.min(base, themedFontSize(LEAD_MIN, context, false)),
|
|
2166
|
+
maxWidthPx: width,
|
|
2167
|
+
maxHeightPx: (area.bottom - area.top) * 0.9,
|
|
2168
|
+
lineHeight: LEAD_LINE_HEIGHT
|
|
2169
|
+
});
|
|
2170
|
+
const top = area.top + Math.max(0, (area.bottom - area.top - fit.heightPx) * 0.25);
|
|
2171
|
+
return [
|
|
2172
|
+
...area.layers,
|
|
2173
|
+
...step(
|
|
2174
|
+
[
|
|
2175
|
+
[
|
|
2176
|
+
{
|
|
2177
|
+
type: "shape",
|
|
2178
|
+
id: "lead-bar",
|
|
2179
|
+
position: {
|
|
2180
|
+
x: area.left,
|
|
2181
|
+
y: top + fit.fontSize * 0.2,
|
|
2182
|
+
width: barW,
|
|
2183
|
+
height: fit.heightPx - fit.fontSize * 0.3
|
|
2184
|
+
},
|
|
2185
|
+
content: { shape: "rect", fill: area.accent, borderRadius: barW / 2 }
|
|
2186
|
+
},
|
|
2187
|
+
"bar",
|
|
2188
|
+
0
|
|
2189
|
+
],
|
|
2190
|
+
[
|
|
2191
|
+
textLayer(
|
|
2192
|
+
"lead",
|
|
2193
|
+
text,
|
|
2194
|
+
box(area.left + indent, top, width, fit.heightPx + fit.fontSize * 0.4),
|
|
2195
|
+
{
|
|
2196
|
+
fontSize: fit.fontSize,
|
|
2197
|
+
fontFamily: getThemeFont(context, "body"),
|
|
2198
|
+
color: theme.colors.text,
|
|
2199
|
+
lineHeight: LEAD_LINE_HEIGHT,
|
|
2200
|
+
shadow: shouldUseShadow(context),
|
|
2201
|
+
...fit.maxLines ? { maxLines: fit.maxLines } : {}
|
|
2202
|
+
}
|
|
2203
|
+
),
|
|
2204
|
+
"text",
|
|
2205
|
+
0.15
|
|
2206
|
+
]
|
|
2207
|
+
],
|
|
2208
|
+
0,
|
|
2209
|
+
context
|
|
2210
|
+
)
|
|
2211
|
+
];
|
|
2212
|
+
}
|
|
2213
|
+
var STATEMENT_BASE = 84;
|
|
2214
|
+
var STATEMENT_MIN = 48;
|
|
2215
|
+
function statement(area, text, context) {
|
|
2216
|
+
const { theme, viewport } = context;
|
|
2217
|
+
const W = area.right - area.left;
|
|
2218
|
+
const H = area.bottom - area.top;
|
|
2219
|
+
const base = themedFontSize(STATEMENT_BASE, context, true);
|
|
2220
|
+
const fit = fitProse({
|
|
2221
|
+
text,
|
|
2222
|
+
baseFontSize: base,
|
|
2223
|
+
minFontSize: Math.min(base, themedFontSize(STATEMENT_MIN, context, true)),
|
|
2224
|
+
maxWidthPx: W * (area.portrait ? 1 : 0.86),
|
|
2225
|
+
maxHeightPx: H * 0.8,
|
|
2226
|
+
lineHeight: HEADLINE_LINE_HEIGHT
|
|
2227
|
+
});
|
|
2228
|
+
const top = area.top + Math.max(0, (H - fit.heightPx) / 2);
|
|
2229
|
+
const ruleH = Math.max(5, Math.round(viewport.height * 7e-3));
|
|
2230
|
+
return [
|
|
2231
|
+
...area.layers,
|
|
2232
|
+
...step(
|
|
2233
|
+
[
|
|
2234
|
+
[
|
|
2235
|
+
{
|
|
2236
|
+
type: "shape",
|
|
2237
|
+
id: "statement-rule",
|
|
2238
|
+
position: {
|
|
2239
|
+
x: area.left,
|
|
2240
|
+
y: top - ruleH - fit.fontSize * 0.45,
|
|
2241
|
+
width: W * 0.06,
|
|
2242
|
+
height: ruleH
|
|
2243
|
+
},
|
|
2244
|
+
content: { shape: "rect", fill: area.accent, borderRadius: ruleH / 2 }
|
|
2245
|
+
},
|
|
2246
|
+
"rule",
|
|
2247
|
+
0
|
|
2248
|
+
],
|
|
2249
|
+
[
|
|
2250
|
+
textLayer(
|
|
2251
|
+
"statement",
|
|
2252
|
+
text,
|
|
2253
|
+
box(area.left, top, W * (area.portrait ? 1 : 0.86), fit.heightPx + fit.fontSize * 0.3),
|
|
2254
|
+
{
|
|
2255
|
+
fontSize: fit.fontSize,
|
|
2256
|
+
fontFamily: getThemeFont(context, "title"),
|
|
2257
|
+
fontWeight: "bold",
|
|
2258
|
+
color: theme.colors.text,
|
|
2259
|
+
lineHeight: HEADLINE_LINE_HEIGHT,
|
|
2260
|
+
shadow: shouldUseShadow(context),
|
|
2261
|
+
...fit.maxLines ? { maxLines: fit.maxLines } : {}
|
|
2262
|
+
}
|
|
2263
|
+
),
|
|
2264
|
+
"words",
|
|
2265
|
+
0.15
|
|
2266
|
+
]
|
|
2267
|
+
],
|
|
2268
|
+
0,
|
|
2269
|
+
context
|
|
2270
|
+
)
|
|
2271
|
+
];
|
|
2272
|
+
}
|
|
2273
|
+
function cardsBlock(input, context) {
|
|
2274
|
+
const items = itemsOf(input);
|
|
2275
|
+
const area = frame(input, context);
|
|
2276
|
+
const { theme, viewport } = context;
|
|
2277
|
+
if (!items.length) return area.layers;
|
|
2278
|
+
if (items.length === 1 && !items[0].label)
|
|
2279
|
+
return (input.title?.trim() ? lead : statement)(area, items[0].detail, context);
|
|
2280
|
+
const contrast = String(input.versus) === "true";
|
|
2281
|
+
if (contrast && items.length === 2 && !area.portrait) return versus(area, items, context);
|
|
2282
|
+
const W = area.right - area.left;
|
|
2283
|
+
const H = area.bottom - area.top;
|
|
2284
|
+
const brief = items.every((item) => item.label.length + item.detail.length <= 28);
|
|
2285
|
+
const columns2 = area.portrait ? 1 : items.length <= 3 ? items.length : items.length === 4 ? brief ? 4 : 2 : 3;
|
|
2286
|
+
const rows = Math.ceil(items.length / columns2);
|
|
2287
|
+
const gap = Math.round(viewport.width * 0.02);
|
|
2288
|
+
const cardW = (W - gap * (columns2 - 1)) / columns2;
|
|
2289
|
+
const pad = Math.round(Math.min(44, Math.max(26, cardW * 0.08)));
|
|
2290
|
+
const ruleH = Math.max(4, Math.round(viewport.height * 6e-3));
|
|
2291
|
+
const ruleGap = Math.round(viewport.height * 0.022);
|
|
2292
|
+
const chrome = pad * 2 + ruleH + ruleGap;
|
|
2293
|
+
const { sizes } = fitRamp(
|
|
2294
|
+
items,
|
|
2295
|
+
context,
|
|
2296
|
+
() => cardW - pad * 2,
|
|
2297
|
+
() => (H - gap * (rows - 1)) / rows - chrome
|
|
2298
|
+
);
|
|
2299
|
+
const measures = items.map((item) => measureItem(item, sizes, cardW - pad * 2));
|
|
2300
|
+
const cardH = Math.min(
|
|
2301
|
+
(H - gap * (rows - 1)) / rows,
|
|
2302
|
+
Math.max(...measures.map((m) => m.height)) + chrome + sizes.detail * 0.4
|
|
2303
|
+
);
|
|
2304
|
+
const gridH = rows * cardH + (rows - 1) * gap;
|
|
2305
|
+
const gridTop = area.top + Math.max(0, (H - gridH) * 0.35);
|
|
2306
|
+
const layers = [...area.layers];
|
|
2307
|
+
items.forEach((item, index) => {
|
|
2308
|
+
const row = Math.floor(index / columns2);
|
|
2309
|
+
const inRow = Math.min(columns2, items.length - row * columns2);
|
|
2310
|
+
const rowLeft = area.left + (columns2 - inRow) * (cardW + gap) / 2;
|
|
2311
|
+
const left = rowLeft + index % columns2 * (cardW + gap);
|
|
2312
|
+
const top = gridTop + row * (cardH + gap);
|
|
2313
|
+
const card = {
|
|
2314
|
+
type: "shape",
|
|
2315
|
+
id: `card-${index}`,
|
|
2316
|
+
position: { x: left, y: top, width: cardW, height: cardH },
|
|
2317
|
+
content: {
|
|
2318
|
+
shape: "rect",
|
|
2319
|
+
fill: withAlpha(theme.colors.text, 0.06),
|
|
2320
|
+
stroke: withAlpha(theme.colors.text, 0.12),
|
|
2321
|
+
strokeWidth: 2,
|
|
2322
|
+
borderRadius: Math.round(Math.min(cardW, cardH) * 0.06)
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
const rule = {
|
|
2326
|
+
type: "shape",
|
|
2327
|
+
id: `card-${index}-rule`,
|
|
2328
|
+
position: { x: left + pad, y: top + pad, width: Math.min(cardW * 0.22, 64), height: ruleH },
|
|
2329
|
+
content: { shape: "rect", fill: area.accent, borderRadius: ruleH / 2 }
|
|
2330
|
+
};
|
|
2331
|
+
layers.push(
|
|
2332
|
+
...step(
|
|
2333
|
+
[
|
|
2334
|
+
[card, "surface", 0],
|
|
2335
|
+
[rule, "rule", 0.2],
|
|
2336
|
+
...words(
|
|
2337
|
+
itemText(
|
|
2338
|
+
`card-${index}`,
|
|
2339
|
+
item,
|
|
2340
|
+
measures[index],
|
|
2341
|
+
sizes,
|
|
2342
|
+
left + pad,
|
|
2343
|
+
top + pad + ruleH + ruleGap,
|
|
2344
|
+
cardW - pad * 2,
|
|
2345
|
+
"left",
|
|
2346
|
+
context
|
|
2347
|
+
),
|
|
2348
|
+
0.22
|
|
2349
|
+
)
|
|
2350
|
+
],
|
|
2351
|
+
index,
|
|
2352
|
+
context
|
|
2353
|
+
)
|
|
2354
|
+
);
|
|
2355
|
+
});
|
|
2356
|
+
return layers;
|
|
2357
|
+
}
|
|
2358
|
+
function versus(area, items, context) {
|
|
2359
|
+
const { theme, viewport } = context;
|
|
2360
|
+
const W = area.right - area.left;
|
|
2361
|
+
const H = area.bottom - area.top;
|
|
2362
|
+
const seam = Math.round(viewport.width * 0.08);
|
|
2363
|
+
const panelW = (W - seam) / 2;
|
|
2364
|
+
const pad = Math.round(Math.min(56, Math.max(30, panelW * 0.08)));
|
|
2365
|
+
const ruleH = Math.max(4, Math.round(viewport.height * 6e-3));
|
|
2366
|
+
const ruleGap = Math.round(viewport.height * 0.022);
|
|
2367
|
+
const chrome = pad * 2 + ruleH + ruleGap;
|
|
2368
|
+
const { sizes } = fitRamp(
|
|
2369
|
+
items,
|
|
2370
|
+
context,
|
|
2371
|
+
() => panelW - pad * 2,
|
|
2372
|
+
() => H * 0.8 - chrome
|
|
2373
|
+
);
|
|
2374
|
+
const measures = items.map((item) => measureItem(item, sizes, panelW - pad * 2));
|
|
2375
|
+
const panelH = Math.min(
|
|
2376
|
+
H,
|
|
2377
|
+
Math.max(...measures.map((m) => m.height)) + chrome + sizes.detail * 0.4
|
|
2378
|
+
);
|
|
2379
|
+
const top = area.top + Math.max(0, (H - panelH) * 0.35);
|
|
2380
|
+
const mark = Math.round(Math.min(viewport.height * 0.1, seam * 0.85));
|
|
2381
|
+
const markFill = area.accent;
|
|
2382
|
+
const layers = [...area.layers];
|
|
2383
|
+
items.forEach((item, index) => {
|
|
2384
|
+
const left = area.left + index * (panelW + seam);
|
|
2385
|
+
const radius = Math.round(Math.min(panelW, panelH) * 0.05);
|
|
2386
|
+
const parts = [
|
|
2387
|
+
[
|
|
2388
|
+
{
|
|
2389
|
+
type: "shape",
|
|
2390
|
+
id: `side-${index}`,
|
|
2391
|
+
position: { x: left, y: top, width: panelW, height: panelH },
|
|
2392
|
+
content: {
|
|
2393
|
+
shape: "rect",
|
|
2394
|
+
fill: withAlpha(theme.colors.text, 0.06),
|
|
2395
|
+
stroke: withAlpha(theme.colors.text, 0.12),
|
|
2396
|
+
strokeWidth: 2,
|
|
2397
|
+
borderRadius: radius
|
|
2398
|
+
}
|
|
2399
|
+
},
|
|
2400
|
+
"surface",
|
|
2401
|
+
0
|
|
2402
|
+
],
|
|
2403
|
+
[
|
|
2404
|
+
{
|
|
2405
|
+
type: "shape",
|
|
2406
|
+
id: `side-${index}-rule`,
|
|
2407
|
+
position: {
|
|
2408
|
+
x: left + pad,
|
|
2409
|
+
y: top + pad,
|
|
2410
|
+
width: Math.min(panelW * 0.22, 64),
|
|
2411
|
+
height: ruleH
|
|
2412
|
+
},
|
|
2413
|
+
content: { shape: "rect", fill: area.accent, borderRadius: ruleH / 2 }
|
|
2414
|
+
},
|
|
2415
|
+
"rule",
|
|
2416
|
+
0.15
|
|
2417
|
+
],
|
|
2418
|
+
...words(
|
|
2419
|
+
itemText(
|
|
2420
|
+
`side-${index}`,
|
|
2421
|
+
item,
|
|
2422
|
+
measures[index],
|
|
2423
|
+
sizes,
|
|
2424
|
+
left + pad,
|
|
2425
|
+
top + pad + ruleH + ruleGap,
|
|
2426
|
+
panelW - pad * 2,
|
|
2427
|
+
"left",
|
|
2428
|
+
context
|
|
2429
|
+
),
|
|
2430
|
+
0.2
|
|
2431
|
+
)
|
|
2432
|
+
];
|
|
2433
|
+
layers.push(...step(parts, index, context));
|
|
2434
|
+
});
|
|
2435
|
+
const cx = area.left + panelW + seam / 2;
|
|
2436
|
+
const cy = top + panelH / 2;
|
|
2437
|
+
const at = FIRST_TURN + 0.45;
|
|
2438
|
+
layers.push(
|
|
2439
|
+
{
|
|
2440
|
+
type: "shape",
|
|
2441
|
+
id: "versus-mark",
|
|
2442
|
+
position: { x: cx - mark / 2, y: cy - mark / 2, width: mark, height: mark },
|
|
2443
|
+
content: { shape: "circle", fill: markFill },
|
|
2444
|
+
animation: entrance(context, "pop", at)
|
|
2445
|
+
},
|
|
2446
|
+
{
|
|
2447
|
+
type: "text",
|
|
2448
|
+
id: "versus-mark-text",
|
|
2449
|
+
position: { x: cx, y: cy, width: mark, height: mark, anchor: "center" },
|
|
2450
|
+
content: {
|
|
2451
|
+
text: "vs",
|
|
2452
|
+
style: {
|
|
2453
|
+
fontSize: Math.round(mark * 0.38),
|
|
2454
|
+
fontFamily: getThemeFont(context, "title"),
|
|
2455
|
+
fontWeight: "bold",
|
|
2456
|
+
fontStyle: "italic",
|
|
2457
|
+
color: pickContrastingText(markFill),
|
|
2458
|
+
textAlign: "center",
|
|
2459
|
+
verticalAlign: "middle"
|
|
2460
|
+
}
|
|
2461
|
+
},
|
|
2462
|
+
animation: entrance(context, "pop", at)
|
|
2463
|
+
}
|
|
2464
|
+
);
|
|
2465
|
+
return layers;
|
|
2466
|
+
}
|
|
2467
|
+
function badge(id, number, cx, cy, diameter, accent, context) {
|
|
2468
|
+
return [
|
|
2469
|
+
{
|
|
2470
|
+
type: "shape",
|
|
2471
|
+
id,
|
|
2472
|
+
position: { x: cx - diameter / 2, y: cy - diameter / 2, width: diameter, height: diameter },
|
|
2473
|
+
content: { shape: "circle", fill: accent }
|
|
2474
|
+
},
|
|
2475
|
+
{
|
|
2476
|
+
type: "text",
|
|
2477
|
+
id: `${id}-number`,
|
|
2478
|
+
position: { x: cx, y: cy, width: diameter, height: diameter, anchor: "center" },
|
|
2479
|
+
content: {
|
|
2480
|
+
text: String(number),
|
|
2481
|
+
style: {
|
|
2482
|
+
fontSize: Math.round(diameter * 0.44),
|
|
2483
|
+
fontFamily: getThemeFont(context, "title"),
|
|
2484
|
+
fontWeight: "bold",
|
|
2485
|
+
color: pickContrastingText(accent),
|
|
2486
|
+
textAlign: "center",
|
|
2487
|
+
verticalAlign: "middle",
|
|
2488
|
+
baselineShift: 0.04
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
];
|
|
2493
|
+
}
|
|
2494
|
+
function connector(id, d, color, width, arrow = true) {
|
|
2495
|
+
return {
|
|
2496
|
+
type: "path",
|
|
2497
|
+
id,
|
|
2498
|
+
position: { x: 0, y: 0, width: "100%", height: "100%" },
|
|
2499
|
+
content: {
|
|
2500
|
+
d,
|
|
2501
|
+
stroke: color,
|
|
2502
|
+
strokeWidth: width,
|
|
2503
|
+
fill: "none",
|
|
2504
|
+
...arrow ? { endMarker: "arrow" } : {}
|
|
2505
|
+
}
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
function processBlock(input, context) {
|
|
2509
|
+
const items = itemsOf(input);
|
|
2510
|
+
const area = frame(input, context);
|
|
2511
|
+
const { viewport } = context;
|
|
2512
|
+
if (!items.length) return area.layers;
|
|
2513
|
+
const dated = timelineItems(items);
|
|
2514
|
+
if (dated) return timeline(area, dated, context);
|
|
2515
|
+
const W = area.right - area.left;
|
|
2516
|
+
const H = area.bottom - area.top;
|
|
2517
|
+
const line = withAlpha(area.accent, 0.75);
|
|
2518
|
+
const stroke = Math.max(3, Math.round(viewport.height * 4e-3));
|
|
2519
|
+
const layers = [...area.layers];
|
|
2520
|
+
const across = !area.portrait && items.length <= 4 && items.every((item) => item.detail.length <= 110 && item.label.length <= 40);
|
|
2521
|
+
if (across) {
|
|
2522
|
+
const columnW = W / items.length;
|
|
2523
|
+
const diameter2 = Math.round(Math.min(viewport.height * 0.1, columnW * 0.3));
|
|
2524
|
+
const textGap = Math.round(diameter2 * 0.45);
|
|
2525
|
+
const textW2 = columnW - Math.round(viewport.width * 0.02);
|
|
2526
|
+
const { sizes: sizes2 } = fitRamp(
|
|
2527
|
+
items,
|
|
2528
|
+
context,
|
|
2529
|
+
() => textW2,
|
|
2530
|
+
() => H - diameter2 - textGap
|
|
2531
|
+
);
|
|
2532
|
+
const measures2 = items.map((item) => measureItem(item, sizes2, textW2));
|
|
2533
|
+
const groupH = diameter2 + textGap + Math.max(...measures2.map((m) => m.height));
|
|
2534
|
+
const groupTop = area.top + Math.max(0, (H - groupH) * 0.2);
|
|
2535
|
+
const cy = groupTop + diameter2 / 2;
|
|
2536
|
+
items.forEach((item, index) => {
|
|
2537
|
+
const cx2 = area.left + columnW * (index + 0.5);
|
|
2538
|
+
const parts = [];
|
|
2539
|
+
const arrives = index > 0 ? 0.3 : 0;
|
|
2540
|
+
if (index > 0) {
|
|
2541
|
+
const from = cx2 - columnW + diameter2 / 2 + diameter2 * 0.25;
|
|
2542
|
+
const to = cx2 - diameter2 / 2 - diameter2 * 0.25;
|
|
2543
|
+
parts.push([
|
|
2544
|
+
connector(`step-${index}-arrow`, `M ${from} ${cy} L ${to} ${cy}`, line, stroke),
|
|
2545
|
+
"draw",
|
|
2546
|
+
0
|
|
2547
|
+
]);
|
|
2548
|
+
}
|
|
2549
|
+
parts.push(
|
|
2550
|
+
...pops(badge(`step-${index}`, index + 1, cx2, cy, diameter2, area.accent, context), arrives)
|
|
2551
|
+
);
|
|
2552
|
+
parts.push(
|
|
2553
|
+
...words(
|
|
2554
|
+
itemText(
|
|
2555
|
+
`step-${index}`,
|
|
2556
|
+
item,
|
|
2557
|
+
measures2[index],
|
|
2558
|
+
sizes2,
|
|
2559
|
+
cx2 - textW2 / 2,
|
|
2560
|
+
groupTop + diameter2 + textGap,
|
|
2561
|
+
textW2,
|
|
2562
|
+
"center",
|
|
2563
|
+
context
|
|
2564
|
+
),
|
|
2565
|
+
arrives + 0.18
|
|
2566
|
+
)
|
|
2567
|
+
);
|
|
2568
|
+
layers.push(...step(parts, index, context));
|
|
2569
|
+
});
|
|
2570
|
+
return layers;
|
|
2571
|
+
}
|
|
2572
|
+
const diameter = Math.round(Math.min(viewport.height * 0.07, viewport.width * 0.07));
|
|
2573
|
+
const textLeft = area.left + diameter + Math.round(viewport.width * 0.022);
|
|
2574
|
+
const textW = area.right - textLeft;
|
|
2575
|
+
const rowGap = Math.round(viewport.height * 0.028);
|
|
2576
|
+
const { sizes } = fitRamp(
|
|
2577
|
+
items,
|
|
2578
|
+
context,
|
|
2579
|
+
() => textW,
|
|
2580
|
+
() => (H - rowGap * (items.length - 1)) / items.length
|
|
2581
|
+
);
|
|
2582
|
+
const measures = items.map((item) => measureItem(item, sizes, textW));
|
|
2583
|
+
const rowH = measures.map((m) => Math.max(diameter, m.height));
|
|
2584
|
+
const rowsH = rowH.reduce((sum, h) => sum + h, 0);
|
|
2585
|
+
const gap = items.length > 1 ? rowGap + Math.min(
|
|
2586
|
+
viewport.height * 0.05,
|
|
2587
|
+
Math.max(0, (H - rowsH - rowGap * (items.length - 1)) * 0.5) / (items.length - 1)
|
|
2588
|
+
) : 0;
|
|
2589
|
+
const totalH = rowsH + gap * (items.length - 1);
|
|
2590
|
+
let top = area.top + Math.max(0, (H - totalH) * 0.2);
|
|
2591
|
+
const cx = area.left + diameter / 2;
|
|
2592
|
+
let previous = 0;
|
|
2593
|
+
items.forEach((item, index) => {
|
|
2594
|
+
const parts = [];
|
|
2595
|
+
const firstLine = item.label ? sizes.label * LABEL_LINE_HEIGHT : sizes.plain * DETAIL_LINE_HEIGHT;
|
|
2596
|
+
const cy = top + Math.max(firstLine, diameter) / 2;
|
|
2597
|
+
let arrives = 0;
|
|
2598
|
+
if (index > 0) {
|
|
2599
|
+
const from = previous + diameter / 2 + diameter * 0.12;
|
|
2600
|
+
const to = cy - diameter / 2 - diameter * 0.12;
|
|
2601
|
+
if (to > from) {
|
|
2602
|
+
parts.push([
|
|
2603
|
+
connector(
|
|
2604
|
+
`step-${index}-arrow`,
|
|
2605
|
+
`M ${cx} ${from} L ${cx} ${to}`,
|
|
2606
|
+
line,
|
|
2607
|
+
stroke,
|
|
2608
|
+
to - from >= diameter * 0.6
|
|
2609
|
+
),
|
|
2610
|
+
"draw",
|
|
2611
|
+
0
|
|
2612
|
+
]);
|
|
2613
|
+
arrives = 0.25;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
previous = cy;
|
|
2617
|
+
parts.push(
|
|
2618
|
+
...pops(badge(`step-${index}`, index + 1, cx, cy, diameter, area.accent, context), arrives)
|
|
2619
|
+
);
|
|
2620
|
+
parts.push(
|
|
2621
|
+
...words(
|
|
2622
|
+
itemText(
|
|
2623
|
+
`step-${index}`,
|
|
2624
|
+
item,
|
|
2625
|
+
measures[index],
|
|
2626
|
+
sizes,
|
|
2627
|
+
textLeft,
|
|
2628
|
+
top,
|
|
2629
|
+
textW,
|
|
2630
|
+
"left",
|
|
2631
|
+
context
|
|
2632
|
+
),
|
|
2633
|
+
arrives + 0.15
|
|
2634
|
+
)
|
|
2635
|
+
);
|
|
2636
|
+
layers.push(...step(parts, index, context));
|
|
2637
|
+
top += rowH[index] + gap;
|
|
2638
|
+
});
|
|
2639
|
+
return layers;
|
|
2640
|
+
}
|
|
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
|
+
function timeline(area, items, context) {
|
|
2666
|
+
const { viewport } = context;
|
|
2667
|
+
const W = area.right - area.left;
|
|
2668
|
+
const H = area.bottom - area.top;
|
|
2669
|
+
const n = items.length;
|
|
2670
|
+
const dot = Math.max(14, Math.round(viewport.height * 0.026));
|
|
2671
|
+
const whenSize = Math.round(themedFontSize(LABEL_BASE, context, true) * 1.1);
|
|
2672
|
+
const whenH = whenSize * LABEL_LINE_HEIGHT;
|
|
2673
|
+
const gap = Math.round(viewport.height * 0.03);
|
|
2674
|
+
const stroke = Math.max(3, Math.round(viewport.height * 4e-3));
|
|
2675
|
+
const line = withAlpha(area.accent, 0.6);
|
|
2676
|
+
const layers = [...area.layers];
|
|
2677
|
+
const station = (index, cx, cy) => ({
|
|
2678
|
+
type: "shape",
|
|
2679
|
+
id: `station-${index}`,
|
|
2680
|
+
position: { x: cx - dot / 2, y: cy - dot / 2, width: dot, height: dot },
|
|
2681
|
+
content: { shape: "circle", fill: area.accent }
|
|
2682
|
+
});
|
|
2683
|
+
const whenText = (index, item, left, top2, width, align) => textLayer(`station-${index}-when`, item.when, box(left, top2, width, whenH + whenSize * 0.3), {
|
|
2684
|
+
fontSize: whenSize,
|
|
2685
|
+
fontFamily: getThemeFont(context, "title"),
|
|
2686
|
+
fontWeight: "bold",
|
|
2687
|
+
color: area.accent,
|
|
2688
|
+
lineHeight: LABEL_LINE_HEIGHT,
|
|
2689
|
+
textAlign: align
|
|
2690
|
+
});
|
|
2691
|
+
const columnW = W / n;
|
|
2692
|
+
const across = !area.portrait && n <= 5 && items.every((item) => item.detail.length <= 120 && item.label.length <= 40);
|
|
2693
|
+
if (across) {
|
|
2694
|
+
const textW2 = columnW - Math.round(viewport.width * 0.02);
|
|
2695
|
+
const { sizes: sizes2 } = fitRamp(
|
|
2696
|
+
items,
|
|
2697
|
+
context,
|
|
2698
|
+
() => textW2,
|
|
2699
|
+
() => H - whenH - dot - gap * 2
|
|
2700
|
+
);
|
|
2701
|
+
const measures2 = items.map((item) => measureItem(item, sizes2, textW2));
|
|
2702
|
+
const groupH = whenH + gap + dot + gap + Math.max(...measures2.map((m) => m.height));
|
|
2703
|
+
const groupTop = area.top + Math.max(0, (H - groupH) * 0.3);
|
|
2704
|
+
const trackY = groupTop + whenH + gap + dot / 2;
|
|
2705
|
+
let from2 = area.left;
|
|
2706
|
+
items.forEach((item, index) => {
|
|
2707
|
+
const cx = area.left + columnW * (index + 0.5);
|
|
2708
|
+
const parts = [
|
|
2709
|
+
[
|
|
2710
|
+
connector(
|
|
2711
|
+
`station-${index}-track`,
|
|
2712
|
+
`M ${from2} ${trackY} L ${cx - dot} ${trackY}`,
|
|
2713
|
+
line,
|
|
2714
|
+
stroke,
|
|
2715
|
+
false
|
|
2716
|
+
),
|
|
2717
|
+
"draw",
|
|
2718
|
+
0
|
|
2719
|
+
],
|
|
2720
|
+
[station(index, cx, trackY), "pop", 0.3],
|
|
2721
|
+
[whenText(index, item, cx - textW2 / 2, groupTop, textW2, "center"), "text", 0.35],
|
|
2722
|
+
...words(
|
|
2723
|
+
itemText(
|
|
2724
|
+
`station-${index}`,
|
|
2725
|
+
item,
|
|
2726
|
+
measures2[index],
|
|
2727
|
+
sizes2,
|
|
2728
|
+
cx - textW2 / 2,
|
|
2729
|
+
trackY + dot / 2 + gap,
|
|
2730
|
+
textW2,
|
|
2731
|
+
"center",
|
|
2732
|
+
context
|
|
2733
|
+
),
|
|
2734
|
+
0.47
|
|
2735
|
+
)
|
|
2736
|
+
];
|
|
2737
|
+
if (index === n - 1)
|
|
2738
|
+
parts.push([
|
|
2739
|
+
connector(
|
|
2740
|
+
"station-tail",
|
|
2741
|
+
`M ${cx + dot} ${trackY} L ${area.right} ${trackY}`,
|
|
2742
|
+
withAlpha(area.accent, 0.3),
|
|
2743
|
+
stroke
|
|
2744
|
+
),
|
|
2745
|
+
"draw",
|
|
2746
|
+
0.65
|
|
2747
|
+
]);
|
|
2748
|
+
from2 = cx + dot;
|
|
2749
|
+
layers.push(...step(parts, index, context));
|
|
2750
|
+
});
|
|
2751
|
+
return layers;
|
|
2752
|
+
}
|
|
2753
|
+
const whenW = Math.min(
|
|
2754
|
+
W * 0.28,
|
|
2755
|
+
Math.max(...items.map((item) => item.when.length)) * whenSize * 0.62 + whenSize * 0.2
|
|
2756
|
+
);
|
|
2757
|
+
const trackX = area.left + whenW + gap + dot / 2;
|
|
2758
|
+
const textLeft = trackX + dot / 2 + gap;
|
|
2759
|
+
const textW = area.right - textLeft;
|
|
2760
|
+
const rowGap = Math.round(viewport.height * 0.028);
|
|
2761
|
+
const { sizes } = fitRamp(
|
|
2762
|
+
items,
|
|
2763
|
+
context,
|
|
2764
|
+
() => textW,
|
|
2765
|
+
() => (H - rowGap * (n - 1)) / n
|
|
2766
|
+
);
|
|
2767
|
+
const measures = items.map((item) => measureItem(item, sizes, textW));
|
|
2768
|
+
const rowH = measures.map((m) => Math.max(whenH, m.height));
|
|
2769
|
+
const rowsH = rowH.reduce((sum, h) => sum + h, 0);
|
|
2770
|
+
const spread = n > 1 ? rowGap + Math.min(
|
|
2771
|
+
viewport.height * 0.05,
|
|
2772
|
+
Math.max(0, (H - rowsH - rowGap * (n - 1)) * 0.5) / (n - 1)
|
|
2773
|
+
) : 0;
|
|
2774
|
+
let top = area.top + Math.max(0, (H - rowsH - spread * (n - 1)) * 0.2);
|
|
2775
|
+
let from = 0;
|
|
2776
|
+
items.forEach((item, index) => {
|
|
2777
|
+
const cy = top + whenH / 2;
|
|
2778
|
+
const parts = [];
|
|
2779
|
+
if (index > 0)
|
|
2780
|
+
parts.push([
|
|
2781
|
+
connector(
|
|
2782
|
+
`station-${index}-track`,
|
|
2783
|
+
`M ${trackX} ${from} L ${trackX} ${cy - dot}`,
|
|
2784
|
+
line,
|
|
2785
|
+
stroke,
|
|
2786
|
+
false
|
|
2787
|
+
),
|
|
2788
|
+
"draw",
|
|
2789
|
+
0
|
|
2790
|
+
]);
|
|
2791
|
+
parts.push(
|
|
2792
|
+
[station(index, trackX, cy), "pop", index > 0 ? 0.3 : 0],
|
|
2793
|
+
[whenText(index, item, area.left, top, whenW, "right"), "text", 0.35],
|
|
2794
|
+
...words(
|
|
2795
|
+
itemText(
|
|
2796
|
+
`station-${index}`,
|
|
2797
|
+
item,
|
|
2798
|
+
measures[index],
|
|
2799
|
+
sizes,
|
|
2800
|
+
textLeft,
|
|
2801
|
+
top,
|
|
2802
|
+
textW,
|
|
2803
|
+
"left",
|
|
2804
|
+
context
|
|
2805
|
+
),
|
|
2806
|
+
0.47
|
|
2807
|
+
)
|
|
2808
|
+
);
|
|
2809
|
+
from = cy + dot;
|
|
2810
|
+
top += rowH[index] + spread;
|
|
2811
|
+
if (index === n - 1)
|
|
2812
|
+
parts.push([
|
|
2813
|
+
connector(
|
|
2814
|
+
"station-tail",
|
|
2815
|
+
`M ${trackX} ${from} L ${trackX} ${Math.min(area.bottom, from + spread + whenH)}`,
|
|
2816
|
+
withAlpha(area.accent, 0.3),
|
|
2817
|
+
stroke
|
|
2818
|
+
),
|
|
2819
|
+
"draw",
|
|
2820
|
+
0.65
|
|
2821
|
+
]);
|
|
2822
|
+
layers.push(...step(parts, index, context));
|
|
2823
|
+
});
|
|
2824
|
+
return layers;
|
|
2825
|
+
}
|
|
2826
|
+
function cycleBlock(input, context) {
|
|
2827
|
+
const items = itemsOf(input);
|
|
2828
|
+
const area = frame(input, context);
|
|
2829
|
+
const { viewport } = context;
|
|
2830
|
+
if (items.length < 2) return processBlock({ ...input, template: "process" }, context);
|
|
2831
|
+
const H = area.bottom - area.top;
|
|
2832
|
+
const n = items.length;
|
|
2833
|
+
const cx = (area.left + area.right) / 2;
|
|
2834
|
+
const cy = area.top + H / 2;
|
|
2835
|
+
const angle = (i) => -Math.PI / 2 + 2 * Math.PI * i / n;
|
|
2836
|
+
const vertical = (i) => Math.abs(Math.cos(angle(i))) < 0.35;
|
|
2837
|
+
const diameter = Math.round(Math.min(viewport.height * 0.1, viewport.width * 0.075));
|
|
2838
|
+
const gap = Math.round(diameter * 0.3);
|
|
2839
|
+
const layout = (radius2) => {
|
|
2840
|
+
const above = H / 2 - radius2 - diameter / 2 - gap;
|
|
2841
|
+
const besideW = (area.right - area.left) / 2 - radius2 - diameter / 2 - gap;
|
|
2842
|
+
const verticalW = Math.round(Math.min(besideW * 2, (area.right - area.left) * 0.5));
|
|
2843
|
+
const labelW2 = (i) => vertical(i) ? verticalW : besideW;
|
|
2844
|
+
const boxH = (i) => vertical(i) ? above : radius2 * 1.1;
|
|
2845
|
+
const scale2 = Math.min(
|
|
2846
|
+
...items.map(
|
|
2847
|
+
(_, i) => fitRamp(
|
|
2848
|
+
[items[i]],
|
|
2849
|
+
context,
|
|
2850
|
+
() => labelW2(i),
|
|
2851
|
+
() => boxH(i)
|
|
2852
|
+
).scale
|
|
2853
|
+
)
|
|
2854
|
+
);
|
|
2855
|
+
return { labelW: labelW2, scale: scale2 };
|
|
2856
|
+
};
|
|
2857
|
+
const radii = [0.3, 0.27, 0.24, 0.21, 0.18].map(
|
|
2858
|
+
(share) => Math.round(Math.max(diameter * 1.2, Math.min(H * share, viewport.width * 0.16)))
|
|
2859
|
+
);
|
|
2860
|
+
const radius = radii.find((r) => layout(r).scale >= 0.85) ?? radii[radii.length - 1];
|
|
2861
|
+
const { labelW, scale } = layout(radius);
|
|
2862
|
+
const sizes = ramp(context, scale);
|
|
2863
|
+
const measures = items.map((item, i) => measureItem(item, sizes, labelW(i)));
|
|
2864
|
+
const line = withAlpha(area.accent, 0.75);
|
|
2865
|
+
const stroke = Math.max(3, Math.round(viewport.height * 4e-3));
|
|
2866
|
+
const clear = (diameter / 2 + diameter * 0.22) / radius;
|
|
2867
|
+
const point = (a) => [cx + radius * Math.cos(a), cy + radius * Math.sin(a)];
|
|
2868
|
+
const layers = [...area.layers];
|
|
2869
|
+
items.forEach((item, index) => {
|
|
2870
|
+
const a = angle(index);
|
|
2871
|
+
const [x, y] = point(a);
|
|
2872
|
+
const parts = [];
|
|
2873
|
+
const arcInto = (from, to, id) => {
|
|
2874
|
+
const [x1, y1] = point(from + clear);
|
|
2875
|
+
const [x2, y2] = point(to - clear);
|
|
2876
|
+
return connector(id, `M ${x1} ${y1} A ${radius} ${radius} 0 0 1 ${x2} ${y2}`, line, stroke);
|
|
2877
|
+
};
|
|
2878
|
+
const arrives = index > 0 ? 0.35 : 0;
|
|
2879
|
+
if (index > 0) parts.push([arcInto(angle(index - 1), a, `stage-${index}-arrow`), "draw", 0]);
|
|
2880
|
+
parts.push(
|
|
2881
|
+
...pops(badge(`stage-${index}`, index + 1, x, y, diameter, area.accent, context), arrives)
|
|
2882
|
+
);
|
|
2883
|
+
const m = measures[index];
|
|
2884
|
+
const cos = Math.cos(a);
|
|
2885
|
+
let left;
|
|
2886
|
+
let top;
|
|
2887
|
+
let align;
|
|
2888
|
+
const width = labelW(index);
|
|
2889
|
+
if (cos > 0.35) {
|
|
2890
|
+
left = x + diameter / 2 + gap;
|
|
2891
|
+
top = y - Math.min(m.height / 2, diameter / 2);
|
|
2892
|
+
align = "left";
|
|
2893
|
+
} else if (cos < -0.35) {
|
|
2894
|
+
left = x - diameter / 2 - gap - width;
|
|
2895
|
+
top = y - Math.min(m.height / 2, diameter / 2);
|
|
2896
|
+
align = "right";
|
|
2897
|
+
} else {
|
|
2898
|
+
left = x - width / 2;
|
|
2899
|
+
top = Math.sin(a) < 0 ? y - diameter / 2 - gap - m.height : y + diameter / 2 + gap;
|
|
2900
|
+
align = "center";
|
|
2901
|
+
}
|
|
2902
|
+
parts.push(
|
|
2903
|
+
...words(
|
|
2904
|
+
itemText(`stage-${index}`, item, m, sizes, left, top, width, align, context),
|
|
2905
|
+
arrives + 0.18
|
|
2906
|
+
)
|
|
2907
|
+
);
|
|
2908
|
+
if (index === n - 1)
|
|
2909
|
+
parts.push([arcInto(a, angle(n), "stage-close-arrow"), "draw", arrives + 0.5]);
|
|
2910
|
+
layers.push(...step(parts, index, context));
|
|
2911
|
+
});
|
|
2912
|
+
return layers;
|
|
2913
|
+
}
|
|
2914
|
+
function layersBlock(input, context) {
|
|
2915
|
+
const items = itemsOf(input);
|
|
2916
|
+
if (items.length < 2) return cardsBlock({ ...input, template: "cards" }, context);
|
|
2917
|
+
const area = frame(input, context);
|
|
2918
|
+
const { theme, viewport } = context;
|
|
2919
|
+
const W = area.right - area.left;
|
|
2920
|
+
const H = area.bottom - area.top;
|
|
2921
|
+
const n = items.length;
|
|
2922
|
+
const portrait = area.portrait;
|
|
2923
|
+
const R = Math.round(portrait ? Math.min(W * 0.4, H * 0.24) : Math.min(H * 0.48, W * 0.22));
|
|
2924
|
+
const cx = portrait ? area.left + W / 2 : area.left + R;
|
|
2925
|
+
const cy = portrait ? area.top + R : area.top + H / 2;
|
|
2926
|
+
const gap = Math.round(viewport.width * (portrait ? 0.02 : 0.05));
|
|
2927
|
+
const textLeft = portrait ? area.left + gap * 1.5 : cx + R + gap;
|
|
2928
|
+
const textW = area.right - textLeft;
|
|
2929
|
+
const textTop = portrait ? cy + R + viewport.height * 0.05 : area.top;
|
|
2930
|
+
const textH = area.bottom - textTop;
|
|
2931
|
+
const rowGap = Math.round(viewport.height * 0.025);
|
|
2932
|
+
const { sizes } = fitRamp(
|
|
2933
|
+
items,
|
|
2934
|
+
context,
|
|
2935
|
+
() => textW,
|
|
2936
|
+
() => (textH - rowGap * (n - 1)) / n
|
|
2937
|
+
);
|
|
2938
|
+
const measures = items.map((item) => measureItem(item, sizes, textW));
|
|
2939
|
+
const rowsH = measures.reduce((sum, m) => sum + m.height, 0) + rowGap * (n - 1);
|
|
2940
|
+
let rowTop = portrait ? textTop : Math.max(area.top, cy - rowsH / 2);
|
|
2941
|
+
const radius = (index) => R * (index + 1) / n;
|
|
2942
|
+
const tint = (index) => withAlpha(area.accent, 0.85 - 0.6 * index / (n - 1));
|
|
2943
|
+
const stroke = Math.max(2, Math.round(viewport.height * 3e-3));
|
|
2944
|
+
const rings = [];
|
|
2945
|
+
const callouts = [];
|
|
2946
|
+
items.forEach((item, index) => {
|
|
2947
|
+
const r = radius(index);
|
|
2948
|
+
rings.unshift(
|
|
2949
|
+
...step(
|
|
2950
|
+
[
|
|
2951
|
+
[
|
|
2952
|
+
{
|
|
2953
|
+
type: "shape",
|
|
2954
|
+
id: `layer-${index}`,
|
|
2955
|
+
position: { x: cx - r, y: cy - r, width: r * 2, height: r * 2 },
|
|
2956
|
+
content: {
|
|
2957
|
+
shape: "circle",
|
|
2958
|
+
fill: tint(index),
|
|
2959
|
+
stroke: theme.colors.background,
|
|
2960
|
+
strokeWidth: stroke
|
|
2961
|
+
}
|
|
2962
|
+
},
|
|
2963
|
+
"pop",
|
|
2964
|
+
0
|
|
2965
|
+
]
|
|
2966
|
+
],
|
|
2967
|
+
index,
|
|
2968
|
+
context
|
|
2969
|
+
)
|
|
2970
|
+
);
|
|
2971
|
+
const m = measures[index];
|
|
2972
|
+
const firstLine = item.label ? sizes.label * LABEL_LINE_HEIGHT : sizes.plain * DETAIL_LINE_HEIGHT;
|
|
2973
|
+
const labelY = rowTop + firstLine / 2;
|
|
2974
|
+
const parts = [];
|
|
2975
|
+
if (portrait) {
|
|
2976
|
+
const swatch = Math.round(sizes.label * 0.55);
|
|
2977
|
+
parts.push([
|
|
2978
|
+
{
|
|
2979
|
+
type: "shape",
|
|
2980
|
+
id: `layer-${index}-swatch`,
|
|
2981
|
+
position: { x: area.left, y: labelY - swatch / 2, width: swatch, height: swatch },
|
|
2982
|
+
content: { shape: "circle", fill: tint(index) }
|
|
2983
|
+
},
|
|
2984
|
+
"pop",
|
|
2985
|
+
0.2
|
|
2986
|
+
]);
|
|
2987
|
+
} else {
|
|
2988
|
+
const inner = index ? radius(index - 1) : 0;
|
|
2989
|
+
const middle = index ? (inner + r) / 2 : r * 0.45;
|
|
2990
|
+
const angle = (-40 + 80 * index / Math.max(1, n - 1)) * Math.PI / 180;
|
|
2991
|
+
const x = cx + middle * Math.cos(angle);
|
|
2992
|
+
const y = cy + middle * Math.sin(angle);
|
|
2993
|
+
const dot = Math.max(8, Math.round(viewport.height * 9e-3));
|
|
2994
|
+
parts.push(
|
|
2995
|
+
[
|
|
2996
|
+
connector(
|
|
2997
|
+
`layer-${index}-callout`,
|
|
2998
|
+
`M ${x} ${y} L ${cx + R + gap * 0.3} ${labelY} L ${textLeft - gap * 0.25} ${labelY}`,
|
|
2999
|
+
withAlpha(theme.colors.text, 0.55),
|
|
3000
|
+
stroke,
|
|
3001
|
+
false
|
|
3002
|
+
),
|
|
3003
|
+
"draw",
|
|
3004
|
+
0.25
|
|
3005
|
+
],
|
|
3006
|
+
[
|
|
3007
|
+
{
|
|
3008
|
+
type: "shape",
|
|
3009
|
+
id: `layer-${index}-pin`,
|
|
3010
|
+
position: { x: x - dot / 2, y: y - dot / 2, width: dot, height: dot },
|
|
3011
|
+
content: { shape: "circle", fill: theme.colors.text }
|
|
3012
|
+
},
|
|
3013
|
+
"pop",
|
|
3014
|
+
0.2
|
|
3015
|
+
]
|
|
3016
|
+
);
|
|
3017
|
+
}
|
|
3018
|
+
parts.push(
|
|
3019
|
+
...words(
|
|
3020
|
+
itemText(`layer-${index}`, item, m, sizes, textLeft, rowTop, textW, "left", context),
|
|
3021
|
+
0.4
|
|
3022
|
+
)
|
|
3023
|
+
);
|
|
3024
|
+
callouts.push(...step(parts, index, context));
|
|
3025
|
+
rowTop += m.height + rowGap;
|
|
3026
|
+
});
|
|
3027
|
+
return [...area.layers, ...rings, ...callouts];
|
|
3028
|
+
}
|
|
3029
|
+
var FIGURE_BASE = 280;
|
|
3030
|
+
var FIGURE_MIN = 120;
|
|
3031
|
+
var FIGURE_LINE_HEIGHT = 1;
|
|
3032
|
+
function splitFigure(stat) {
|
|
3033
|
+
const match = stat.trim().match(/^(\D{0,2}[\d.,]+\s?[%×x]?)\s+(\p{L}[\p{L} -]{0,20})$/u);
|
|
3034
|
+
return match ? { number: match[1].trim(), unit: match[2].trim() } : { number: stat.trim(), unit: "" };
|
|
3035
|
+
}
|
|
3036
|
+
function figureLockup(input, context) {
|
|
3037
|
+
const area = frame({ title: input.headline, colorScheme: input.colorScheme }, context);
|
|
3038
|
+
const { theme, viewport } = context;
|
|
3039
|
+
const W = area.right - area.left;
|
|
3040
|
+
const H = area.bottom - area.top;
|
|
3041
|
+
const { number, unit } = splitFigure(input.stat);
|
|
3042
|
+
const gutter = Math.round(viewport.width * 0.05);
|
|
3043
|
+
const figureW = area.portrait ? W : W * 0.4;
|
|
3044
|
+
const textLeft = area.portrait ? area.left : area.left + figureW + gutter;
|
|
3045
|
+
const textW = area.right - textLeft;
|
|
3046
|
+
const base = themedFontSize(FIGURE_BASE, context, true);
|
|
3047
|
+
const figure = fitProse({
|
|
3048
|
+
text: number,
|
|
3049
|
+
baseFontSize: base,
|
|
3050
|
+
minFontSize: Math.min(base, themedFontSize(FIGURE_MIN, context, true)),
|
|
3051
|
+
maxWidthPx: figureW,
|
|
3052
|
+
maxHeightPx: base * FIGURE_LINE_HEIGHT * 1.05,
|
|
3053
|
+
lineHeight: FIGURE_LINE_HEIGHT
|
|
3054
|
+
});
|
|
3055
|
+
const unitSize = themedFontSize(LABEL_BASE, context, true);
|
|
3056
|
+
const unitH = unit ? unitSize * LABEL_LINE_HEIGHT : 0;
|
|
3057
|
+
const figureH = figure.fontSize * 1.08 + unitH;
|
|
3058
|
+
const description = (input.description ?? "").trim();
|
|
3059
|
+
const detail = (input.detail ?? "").trim();
|
|
3060
|
+
const leadBase = themedFontSize(LEAD_BASE, context, false);
|
|
3061
|
+
const room = area.portrait ? H - figureH - viewport.height * 0.05 : H;
|
|
3062
|
+
const lead2 = fitProse({
|
|
3063
|
+
text: description,
|
|
3064
|
+
baseFontSize: leadBase,
|
|
3065
|
+
minFontSize: Math.min(leadBase, themedFontSize(LEAD_MIN, context, false)),
|
|
3066
|
+
maxWidthPx: textW,
|
|
3067
|
+
maxHeightPx: room * (detail ? 0.6 : 0.9),
|
|
3068
|
+
lineHeight: LEAD_LINE_HEIGHT
|
|
3069
|
+
});
|
|
3070
|
+
const detailSize = themedFontSize(DETAIL_BASE, context, false);
|
|
3071
|
+
const detailGap = detail && description ? leadBase * 0.6 : 0;
|
|
3072
|
+
const detailH = detail ? estimateProseLineCount(detail, detailSize, textW) * detailSize * DETAIL_LINE_HEIGHT : 0;
|
|
3073
|
+
const wordsH = (description ? lead2.heightPx : 0) + detailGap + detailH;
|
|
3074
|
+
const groupH = area.portrait ? figureH + viewport.height * 0.05 + wordsH : Math.max(figureH, wordsH);
|
|
3075
|
+
const top = area.top + Math.max(0, (H - groupH) * 0.4);
|
|
3076
|
+
const figureTop = area.portrait ? top : top + Math.max(0, (wordsH - figureH) / 2);
|
|
3077
|
+
const wordsTop = area.portrait ? top + figureH + viewport.height * 0.05 : top + Math.max(0, (figureH - wordsH) / 2);
|
|
3078
|
+
const motion = getMotionProfile(context);
|
|
3079
|
+
const count = motion.countUp ? countUpSpecFor(number) : null;
|
|
3080
|
+
const layers = [...area.layers];
|
|
3081
|
+
layers.push({
|
|
3082
|
+
...textLayer("stat", number, box(area.left, figureTop, figureW, figure.fontSize * 1.08), {
|
|
3083
|
+
fontSize: figure.fontSize,
|
|
3084
|
+
fontFamily: getThemeFont(context, "title"),
|
|
3085
|
+
fontWeight: "bold",
|
|
3086
|
+
color: area.accent,
|
|
3087
|
+
lineHeight: FIGURE_LINE_HEIGHT,
|
|
3088
|
+
shadow: shouldUseShadow(context)
|
|
3089
|
+
}),
|
|
3090
|
+
animation: count ? { type: "countUp", duration: motion.countUpDuration, delay: 0.3, count } : entrance(context, "pop", 0.3)
|
|
3091
|
+
});
|
|
3092
|
+
if (unit)
|
|
3093
|
+
layers.push({
|
|
3094
|
+
...textLayer(
|
|
3095
|
+
"stat-unit",
|
|
3096
|
+
unit,
|
|
3097
|
+
box(area.left, figureTop + figure.fontSize * 1.08, figureW, unitH),
|
|
3098
|
+
{
|
|
3099
|
+
fontSize: unitSize,
|
|
3100
|
+
fontFamily: getThemeFont(context, "title"),
|
|
3101
|
+
fontWeight: "bold",
|
|
3102
|
+
color: area.accent,
|
|
3103
|
+
lineHeight: LABEL_LINE_HEIGHT
|
|
3104
|
+
}
|
|
3105
|
+
),
|
|
3106
|
+
animation: entrance(context, "text", 0.45)
|
|
3107
|
+
});
|
|
3108
|
+
if (!area.portrait && (description || detail))
|
|
3109
|
+
layers.push({
|
|
3110
|
+
type: "shape",
|
|
3111
|
+
id: "stat-divider",
|
|
3112
|
+
position: {
|
|
3113
|
+
x: textLeft - gutter / 2 - 1,
|
|
3114
|
+
y: Math.min(figureTop, wordsTop),
|
|
3115
|
+
width: 2,
|
|
3116
|
+
height: Math.max(figureH, wordsH)
|
|
3117
|
+
},
|
|
3118
|
+
content: { shape: "rect", fill: withAlpha(theme.colors.text, 0.18) },
|
|
3119
|
+
animation: entrance(context, "bar", 0.35)
|
|
3120
|
+
});
|
|
3121
|
+
if (description)
|
|
3122
|
+
layers.push({
|
|
3123
|
+
...textLayer(
|
|
3124
|
+
"description",
|
|
3125
|
+
description,
|
|
3126
|
+
box(textLeft, wordsTop, textW, lead2.heightPx + lead2.fontSize * 0.4),
|
|
3127
|
+
{
|
|
3128
|
+
fontSize: lead2.fontSize,
|
|
3129
|
+
fontFamily: getThemeFont(context, "body"),
|
|
3130
|
+
color: theme.colors.text,
|
|
3131
|
+
lineHeight: LEAD_LINE_HEIGHT,
|
|
3132
|
+
...lead2.maxLines ? { maxLines: lead2.maxLines } : {}
|
|
3133
|
+
}
|
|
3134
|
+
),
|
|
3135
|
+
animation: entrance(context, "text", 0.6)
|
|
3136
|
+
});
|
|
3137
|
+
if (detail)
|
|
3138
|
+
layers.push({
|
|
3139
|
+
...textLayer(
|
|
3140
|
+
"detail",
|
|
3141
|
+
detail,
|
|
3142
|
+
box(
|
|
3143
|
+
textLeft,
|
|
3144
|
+
wordsTop + (description ? lead2.heightPx : 0) + detailGap,
|
|
3145
|
+
textW,
|
|
3146
|
+
detailH + detailSize * 0.4
|
|
3147
|
+
),
|
|
3148
|
+
{
|
|
3149
|
+
fontSize: detailSize,
|
|
3150
|
+
fontFamily: getThemeFont(context, "body"),
|
|
3151
|
+
color: withAlpha(theme.colors.text, 0.8),
|
|
3152
|
+
lineHeight: DETAIL_LINE_HEIGHT
|
|
3153
|
+
}
|
|
3154
|
+
),
|
|
3155
|
+
animation: entrance(context, "text", 0.75)
|
|
3156
|
+
});
|
|
3157
|
+
return layers;
|
|
3158
|
+
}
|
|
3159
|
+
|
|
1927
3160
|
// src/doc/templates/statHighlight.ts
|
|
1928
3161
|
var SAFE_TOP = 0.08;
|
|
1929
3162
|
var SAFE_BOTTOM = 0.92;
|
|
@@ -1931,19 +3164,25 @@ var OPTICAL_CENTER = 0.47;
|
|
|
1931
3164
|
var HERO_MAX_SHARE = 0.38;
|
|
1932
3165
|
var STAT_LINE_HEIGHT = 1.1;
|
|
1933
3166
|
var DESC_LINE_HEIGHT = 1.5;
|
|
1934
|
-
var
|
|
3167
|
+
var DETAIL_LINE_HEIGHT2 = 1.4;
|
|
1935
3168
|
function statHighlight(input, context) {
|
|
1936
|
-
|
|
3169
|
+
if (input.headline?.trim() && !input.accentImage) return figureLockup(input, context);
|
|
3170
|
+
const { stat, colorScheme } = input;
|
|
1937
3171
|
const { theme, viewport } = context;
|
|
1938
3172
|
const accentImage = accentForViewport(input.accentImage, viewport);
|
|
1939
|
-
const colors = resolveColorScheme(context, colorScheme)
|
|
3173
|
+
const colors = colorScheme ? resolveColorScheme(context, colorScheme) : {
|
|
3174
|
+
...resolveColorScheme(context, "blue"),
|
|
3175
|
+
text: theme.colors.highlight || theme.colors.primary,
|
|
3176
|
+
detail: theme.colors.text
|
|
3177
|
+
};
|
|
3178
|
+
const detailColor = "detail" in colors ? colors.detail : colors.text;
|
|
1940
3179
|
const rawDescription = (input.description ?? "").trim();
|
|
1941
3180
|
const description = rawDescription === stat.trim() ? "" : rawDescription;
|
|
1942
3181
|
const detail = (input.detail ?? "").trim();
|
|
1943
3182
|
const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
|
|
1944
3183
|
const statFontSize = themedFontSize(148, context, true);
|
|
1945
|
-
const descFontSize = themedFontSize(
|
|
1946
|
-
const detailFontSize = themedFontSize(
|
|
3184
|
+
const descFontSize = themedFontSize(40, context, false);
|
|
3185
|
+
const detailFontSize = themedFontSize(30, context, false);
|
|
1947
3186
|
const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 180))];
|
|
1948
3187
|
const motion = getMotionProfile(context);
|
|
1949
3188
|
const countSpec = motion.countUp ? countUpSpecFor(stat) : null;
|
|
@@ -1978,8 +3217,8 @@ function statHighlight(input, context) {
|
|
|
1978
3217
|
baseFontSize: detailFontSize,
|
|
1979
3218
|
minFontSize: detailFontSize,
|
|
1980
3219
|
maxWidthPx: columnPx,
|
|
1981
|
-
maxHeightPx: 2 * detailFontSize *
|
|
1982
|
-
lineHeight:
|
|
3220
|
+
maxHeightPx: 2 * detailFontSize * DETAIL_LINE_HEIGHT2,
|
|
3221
|
+
lineHeight: DETAIL_LINE_HEIGHT2
|
|
1983
3222
|
}) : null;
|
|
1984
3223
|
const detailBoxH = detailFit ? detailFit.heightPx + detailFontSize * 0.3 : 0;
|
|
1985
3224
|
const detailGap = detailFit && description ? Math.round(descFontSize * 0.9) : 0;
|
|
@@ -2006,7 +3245,7 @@ function statHighlight(input, context) {
|
|
|
2006
3245
|
const descTop = statTop + statBoxH + gap;
|
|
2007
3246
|
const detailTop = descTop + descBoxH + detailGap;
|
|
2008
3247
|
const pct2 = (px) => `${Number((px / frameH * 100).toFixed(3))}%`;
|
|
2009
|
-
const
|
|
3248
|
+
const box2 = (top, height) => ({
|
|
2010
3249
|
x: accentLayout.textCenterX,
|
|
2011
3250
|
y: pct2(top + height / 2),
|
|
2012
3251
|
width: accentLayout.textWidth,
|
|
@@ -2030,7 +3269,7 @@ function statHighlight(input, context) {
|
|
|
2030
3269
|
...statFit.maxLines ? { maxLines: statFit.maxLines } : {}
|
|
2031
3270
|
}
|
|
2032
3271
|
},
|
|
2033
|
-
position:
|
|
3272
|
+
position: box2(statTop, statBoxH),
|
|
2034
3273
|
animation: statAnimation
|
|
2035
3274
|
});
|
|
2036
3275
|
if (descFit) {
|
|
@@ -2050,7 +3289,7 @@ function statHighlight(input, context) {
|
|
|
2050
3289
|
...descFit.maxLines ? { maxLines: descFit.maxLines } : {}
|
|
2051
3290
|
}
|
|
2052
3291
|
},
|
|
2053
|
-
position:
|
|
3292
|
+
position: box2(descTop, descBoxH),
|
|
2054
3293
|
animation: motionEntrance(context, { type: "fadeIn", duration: 1, delay: 0.3 })
|
|
2055
3294
|
});
|
|
2056
3295
|
}
|
|
@@ -2066,15 +3305,15 @@ function statHighlight(input, context) {
|
|
|
2066
3305
|
// Scheme *text* rather than *accent*: accents in the built-in
|
|
2067
3306
|
// schemes are tuned as chart/fill colors and drop below legible
|
|
2068
3307
|
// contrast as body copy on light surfaces.
|
|
2069
|
-
color:
|
|
3308
|
+
color: detailColor,
|
|
2070
3309
|
textAlign: "left",
|
|
2071
|
-
lineHeight:
|
|
3310
|
+
lineHeight: DETAIL_LINE_HEIGHT2,
|
|
2072
3311
|
shadow: shouldUseShadow(context),
|
|
2073
3312
|
shrinkToFit: true,
|
|
2074
3313
|
...detailFit.maxLines ? { maxLines: detailFit.maxLines } : {}
|
|
2075
3314
|
}
|
|
2076
3315
|
},
|
|
2077
|
-
position:
|
|
3316
|
+
position: box2(detailTop, detailBoxH),
|
|
2078
3317
|
animation: motionEntrance(context, { type: "fadeIn", duration: 1, delay: 1 })
|
|
2079
3318
|
});
|
|
2080
3319
|
}
|
|
@@ -2348,7 +3587,7 @@ function factCard(input, context) {
|
|
|
2348
3587
|
const explainTop = factTop + factBoxH + gap;
|
|
2349
3588
|
const sourceTop = explainTop + explainBoxH + sourceGap;
|
|
2350
3589
|
const pct2 = (px) => `${Number((px / frameH * 100).toFixed(3))}%`;
|
|
2351
|
-
const
|
|
3590
|
+
const box2 = (top, height) => ({
|
|
2352
3591
|
x: accentLayout.textCenterX,
|
|
2353
3592
|
y: pct2(top + height / 2),
|
|
2354
3593
|
width: accentLayout.textWidth,
|
|
@@ -2372,7 +3611,7 @@ function factCard(input, context) {
|
|
|
2372
3611
|
...factFit.maxLines ? { maxLines: factFit.maxLines } : {}
|
|
2373
3612
|
}
|
|
2374
3613
|
},
|
|
2375
|
-
position:
|
|
3614
|
+
position: box2(factTop, factBoxH),
|
|
2376
3615
|
animation: motionEntrance(context, { type: "fadeIn", duration: 1.5 })
|
|
2377
3616
|
});
|
|
2378
3617
|
if (explainFit) {
|
|
@@ -2392,7 +3631,7 @@ function factCard(input, context) {
|
|
|
2392
3631
|
...explainFit.maxLines ? { maxLines: explainFit.maxLines } : {}
|
|
2393
3632
|
}
|
|
2394
3633
|
},
|
|
2395
|
-
position:
|
|
3634
|
+
position: box2(explainTop, explainBoxH),
|
|
2396
3635
|
animation: motionEntrance(context, { type: "fadeIn", duration: 1, delay: 0.8 })
|
|
2397
3636
|
});
|
|
2398
3637
|
}
|
|
@@ -2413,7 +3652,7 @@ function factCard(input, context) {
|
|
|
2413
3652
|
...sourceFit.maxLines ? { maxLines: sourceFit.maxLines } : {}
|
|
2414
3653
|
}
|
|
2415
3654
|
},
|
|
2416
|
-
position:
|
|
3655
|
+
position: box2(sourceTop, sourceBoxH),
|
|
2417
3656
|
animation: { type: "fadeIn", duration: 0.8, delay: 1.5 }
|
|
2418
3657
|
});
|
|
2419
3658
|
}
|
|
@@ -2704,7 +3943,7 @@ function dateEvent(input, context) {
|
|
|
2704
3943
|
const dateTop = groupTopPx;
|
|
2705
3944
|
const descTop = dateTop + dateH + descGap;
|
|
2706
3945
|
const footerTop = descTop + descH + footerGap;
|
|
2707
|
-
const
|
|
3946
|
+
const box2 = (topPx, heightPx) => {
|
|
2708
3947
|
const height = Math.round(heightPx);
|
|
2709
3948
|
return {
|
|
2710
3949
|
x: accentLayout.textCenterX,
|
|
@@ -2754,7 +3993,7 @@ function dateEvent(input, context) {
|
|
|
2754
3993
|
shadow: shouldUseShadow(context)
|
|
2755
3994
|
}
|
|
2756
3995
|
},
|
|
2757
|
-
position:
|
|
3996
|
+
position: box2(dateTop, dateH + dateFit.fontSize * BOX_SLACK_EM),
|
|
2758
3997
|
animation: { type: "fadeIn", duration: 1.5 }
|
|
2759
3998
|
});
|
|
2760
3999
|
if (descFit) {
|
|
@@ -2775,7 +4014,7 @@ function dateEvent(input, context) {
|
|
|
2775
4014
|
shadow: shouldUseShadow(context)
|
|
2776
4015
|
}
|
|
2777
4016
|
},
|
|
2778
|
-
position:
|
|
4017
|
+
position: box2(descTop, descH + descFit.fontSize * BOX_SLACK_EM),
|
|
2779
4018
|
animation: { type: "fadeIn", duration: 2, delay: 1 }
|
|
2780
4019
|
});
|
|
2781
4020
|
}
|
|
@@ -2796,7 +4035,7 @@ function dateEvent(input, context) {
|
|
|
2796
4035
|
shadow: shouldUseShadow(context)
|
|
2797
4036
|
}
|
|
2798
4037
|
},
|
|
2799
|
-
position:
|
|
4038
|
+
position: box2(footerTop, footerH + footerFontSize * BOX_SLACK_EM),
|
|
2800
4039
|
animation: { type: "fadeIn", duration: 1, delay: 3 }
|
|
2801
4040
|
});
|
|
2802
4041
|
}
|
|
@@ -3033,13 +4272,13 @@ function fitWrapWidth(text, fontSize, columnWidthPx, fontFamily) {
|
|
|
3033
4272
|
const mono = isMonoFamily(fontFamily);
|
|
3034
4273
|
if (!mono && !WIDE_GLYPH_RE.test(text)) return columnWidthPx;
|
|
3035
4274
|
const floor = columnWidthPx * 0.5;
|
|
3036
|
-
const
|
|
4275
|
+
const step2 = columnWidthPx * 0.02;
|
|
3037
4276
|
let width = columnWidthPx;
|
|
3038
4277
|
while (width > floor) {
|
|
3039
4278
|
const lines = simulateRendererLines(text, fontSize, width);
|
|
3040
4279
|
const widest = Math.max(...lines.map((line) => estimateLineEm(line, mono))) * fontSize;
|
|
3041
4280
|
if (widest <= columnWidthPx) return width;
|
|
3042
|
-
width -=
|
|
4281
|
+
width -= step2;
|
|
3043
4282
|
}
|
|
3044
4283
|
return floor;
|
|
3045
4284
|
}
|
|
@@ -3407,8 +4646,8 @@ function listItemGeometry(fontSize, baseFontSize, itemCount, textWidthPx, ordere
|
|
|
3407
4646
|
}
|
|
3408
4647
|
function measureListHeightPx(items, fontSize, geometry) {
|
|
3409
4648
|
let heightPx = 0;
|
|
3410
|
-
for (const
|
|
3411
|
-
const lineCount = estimateProseLineCount(
|
|
4649
|
+
for (const itemText2 of items) {
|
|
4650
|
+
const lineCount = estimateProseLineCount(itemText2, fontSize, geometry.bodyWidthPx);
|
|
3412
4651
|
heightPx += lineCount * fontSize * LIST_ITEM_LINE_HEIGHT + geometry.gapPx;
|
|
3413
4652
|
}
|
|
3414
4653
|
return heightPx;
|
|
@@ -3558,9 +4797,9 @@ function listBlock(input, context) {
|
|
|
3558
4797
|
const bodyLeftX = `${leftPct + bodyIndentPx / context.viewport.width * 100}%`;
|
|
3559
4798
|
let itemY = startY;
|
|
3560
4799
|
for (let i = 0; i < items.length; i++) {
|
|
3561
|
-
const
|
|
3562
|
-
const itemHtml = richItems[i]?.text ===
|
|
3563
|
-
const lineCount = estimateProseLineCount(
|
|
4800
|
+
const itemText2 = items[i];
|
|
4801
|
+
const itemHtml = richItems[i]?.text === itemText2 ? richItems[i]?.html : void 0;
|
|
4802
|
+
const lineCount = estimateProseLineCount(itemText2, itemFontSize, bodyWidthPx);
|
|
3564
4803
|
const animation = motionEntrance(context, {
|
|
3565
4804
|
type: "fadeIn",
|
|
3566
4805
|
duration: 0.8,
|
|
@@ -3593,11 +4832,11 @@ function listBlock(input, context) {
|
|
|
3593
4832
|
type: "text",
|
|
3594
4833
|
id: `item-${i}`,
|
|
3595
4834
|
content: {
|
|
3596
|
-
text:
|
|
4835
|
+
text: itemText2,
|
|
3597
4836
|
...itemHtml ? { html: itemHtml } : {},
|
|
3598
4837
|
paragraphs: [
|
|
3599
4838
|
{
|
|
3600
|
-
text:
|
|
4839
|
+
text: itemText2,
|
|
3601
4840
|
list: { ordered, level: 0, ...ordered ? { number: i + 1 } : {} }
|
|
3602
4841
|
}
|
|
3603
4842
|
],
|
|
@@ -3831,7 +5070,7 @@ function definitionCard(input, context) {
|
|
|
3831
5070
|
const defTop = ruleTop + RULE_HEIGHT_PX + defGap;
|
|
3832
5071
|
const originTop = defTop + defH + originGap;
|
|
3833
5072
|
const pct2 = (px) => `${(px / viewport.height * 100).toFixed(3)}%`;
|
|
3834
|
-
const
|
|
5073
|
+
const box2 = (topPx, heightPx) => {
|
|
3835
5074
|
const height = Math.round(heightPx);
|
|
3836
5075
|
return {
|
|
3837
5076
|
x: accentLayout.textCenterX,
|
|
@@ -3869,7 +5108,7 @@ function definitionCard(input, context) {
|
|
|
3869
5108
|
shadow: shouldUseShadow(context)
|
|
3870
5109
|
}
|
|
3871
5110
|
},
|
|
3872
|
-
position:
|
|
5111
|
+
position: box2(termTop, termH + termFit.fontSize * BOX_SLACK_EM2),
|
|
3873
5112
|
animation: motionEntrance(context, { type: "fadeIn", duration: 1.5 })
|
|
3874
5113
|
});
|
|
3875
5114
|
layers.push({
|
|
@@ -3906,7 +5145,7 @@ function definitionCard(input, context) {
|
|
|
3906
5145
|
shadow: shouldUseShadow(context)
|
|
3907
5146
|
}
|
|
3908
5147
|
},
|
|
3909
|
-
position:
|
|
5148
|
+
position: box2(defTop, defH + defFit.fontSize * BOX_SLACK_EM2),
|
|
3910
5149
|
animation: motionEntrance(context, { type: "fadeIn", duration: 1, delay: 0.8 })
|
|
3911
5150
|
});
|
|
3912
5151
|
}
|
|
@@ -3927,7 +5166,7 @@ function definitionCard(input, context) {
|
|
|
3927
5166
|
shadow: shouldUseShadow(context)
|
|
3928
5167
|
}
|
|
3929
5168
|
},
|
|
3930
|
-
position:
|
|
5169
|
+
position: box2(originTop, originH + originFontSize * BOX_SLACK_EM2),
|
|
3931
5170
|
animation: { type: "fadeIn", duration: 0.8, delay: 1.5 }
|
|
3932
5171
|
});
|
|
3933
5172
|
}
|
|
@@ -4638,14 +5877,14 @@ function niceTicks(min, max, targetCount = 5) {
|
|
|
4638
5877
|
}
|
|
4639
5878
|
const count = Math.max(2, targetCount);
|
|
4640
5879
|
const range = niceNum(hi - lo, false);
|
|
4641
|
-
const
|
|
4642
|
-
const niceMin = Math.floor(lo /
|
|
4643
|
-
const niceMax = Math.ceil(hi /
|
|
5880
|
+
const step2 = niceNum(range / (count - 1), true);
|
|
5881
|
+
const niceMin = Math.floor(lo / step2) * step2;
|
|
5882
|
+
const niceMax = Math.ceil(hi / step2) * step2;
|
|
4644
5883
|
const ticks = [];
|
|
4645
|
-
for (let tick = niceMin; tick <= niceMax +
|
|
4646
|
-
ticks.push(Math.abs(tick) <
|
|
5884
|
+
for (let tick = niceMin; tick <= niceMax + step2 * 1e-6; tick += step2) {
|
|
5885
|
+
ticks.push(Math.abs(tick) < step2 * 1e-6 ? 0 : Number(tick.toPrecision(12)));
|
|
4647
5886
|
}
|
|
4648
|
-
return { ticks, niceMin, niceMax, step };
|
|
5887
|
+
return { ticks, niceMin, niceMax, step: step2 };
|
|
4649
5888
|
}
|
|
4650
5889
|
function formatChartValue(value, unit) {
|
|
4651
5890
|
if (!Number.isFinite(value)) return "\u2014";
|
|
@@ -4661,6 +5900,12 @@ function formatChartValue(value, unit) {
|
|
|
4661
5900
|
}
|
|
4662
5901
|
return unit ? `${text} ${unit}` : text;
|
|
4663
5902
|
}
|
|
5903
|
+
function formatChartDataValue(value, unit) {
|
|
5904
|
+
if (!Number.isFinite(value) || Math.abs(value) >= 1e4) return formatChartValue(value, unit);
|
|
5905
|
+
const sign = value < 0 ? "-" : "";
|
|
5906
|
+
const text = `${sign}${Math.abs(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
5907
|
+
return unit ? `${text} ${unit}` : text;
|
|
5908
|
+
}
|
|
4664
5909
|
function trimTrailingZero(text) {
|
|
4665
5910
|
return text.replace(/\.0$/, "");
|
|
4666
5911
|
}
|
|
@@ -4725,16 +5970,16 @@ function computeChartFrame(options) {
|
|
|
4725
5970
|
cursor = titleBand.y + titleBand.height;
|
|
4726
5971
|
}
|
|
4727
5972
|
let tableBand;
|
|
4728
|
-
let
|
|
5973
|
+
let tableHeight2 = 0;
|
|
4729
5974
|
if (options.tableRowCount > 0) {
|
|
4730
|
-
|
|
5975
|
+
tableHeight2 = Math.min(H * 0.4, (options.tableRowCount + 1) * options.tableFontSize * 2.4);
|
|
4731
5976
|
}
|
|
4732
5977
|
const legendHeight = options.legendRows > 0 ? options.legendRows * options.legendRowHeight : 0;
|
|
4733
5978
|
const chartArea = {
|
|
4734
5979
|
x: marginX,
|
|
4735
5980
|
y: cursor,
|
|
4736
5981
|
width: innerWidth,
|
|
4737
|
-
height: Math.max(40, H - marginY - cursor - legendHeight -
|
|
5982
|
+
height: Math.max(40, H - marginY - cursor - legendHeight - tableHeight2)
|
|
4738
5983
|
};
|
|
4739
5984
|
cursor = chartArea.y + chartArea.height;
|
|
4740
5985
|
let legendBand;
|
|
@@ -4742,8 +5987,8 @@ function computeChartFrame(options) {
|
|
|
4742
5987
|
legendBand = { x: marginX, y: cursor, width: innerWidth, height: legendHeight };
|
|
4743
5988
|
cursor += legendHeight;
|
|
4744
5989
|
}
|
|
4745
|
-
if (
|
|
4746
|
-
tableBand = { x: marginX, y: cursor, width: innerWidth, height:
|
|
5990
|
+
if (tableHeight2 > 0) {
|
|
5991
|
+
tableBand = { x: marginX, y: cursor, width: innerWidth, height: tableHeight2 };
|
|
4747
5992
|
}
|
|
4748
5993
|
const plot = {
|
|
4749
5994
|
x: chartArea.x + options.leftGutter,
|
|
@@ -4853,7 +6098,7 @@ function renderChart(kind, input, context) {
|
|
|
4853
6098
|
const innerWidth = viewport.width * 0.88;
|
|
4854
6099
|
const legend = legendLabels.length > 0 ? layoutLegend(legendLabels, legendFontSize, innerWidth) : null;
|
|
4855
6100
|
const gutters = computeGutters(kind, data, input, context, axisFontSize, labelFontSize);
|
|
4856
|
-
const
|
|
6101
|
+
const frame2 = computeChartFrame({
|
|
4857
6102
|
viewportWidth: viewport.width,
|
|
4858
6103
|
viewportHeight: viewport.height,
|
|
4859
6104
|
hasTitle: title.length > 0,
|
|
@@ -4867,7 +6112,7 @@ function renderChart(kind, input, context) {
|
|
|
4867
6112
|
});
|
|
4868
6113
|
const entranceHint = getTemplateHint(context, templateId, "entrance", "staggered");
|
|
4869
6114
|
const markDelay = (index) => entranceHint === "subtle" ? 0.1 : Math.min(1.2, 0.2 + index * 0.08);
|
|
4870
|
-
const
|
|
6115
|
+
const entrance2 = (index) => ({
|
|
4871
6116
|
type: "fadeIn",
|
|
4872
6117
|
duration: 0.6,
|
|
4873
6118
|
delay: markDelay(index)
|
|
@@ -4877,7 +6122,7 @@ function renderChart(kind, input, context) {
|
|
|
4877
6122
|
const growEasing = motion.textEntrance.easing;
|
|
4878
6123
|
const eased = (animation) => growEasing ? { ...animation, easing: growEasing } : animation;
|
|
4879
6124
|
const mark = (index, kind2, along = 0) => {
|
|
4880
|
-
if (!motion.chartGrowth) return
|
|
6125
|
+
if (!motion.chartGrowth) return entrance2(index);
|
|
4881
6126
|
const delay = motionDelay(context, markDelay(index));
|
|
4882
6127
|
switch (kind2) {
|
|
4883
6128
|
case "bar":
|
|
@@ -4920,19 +6165,19 @@ function renderChart(kind, input, context) {
|
|
|
4920
6165
|
input,
|
|
4921
6166
|
context,
|
|
4922
6167
|
data,
|
|
4923
|
-
frame,
|
|
6168
|
+
frame: frame2,
|
|
4924
6169
|
colors,
|
|
4925
6170
|
axisFontSize,
|
|
4926
6171
|
labelFontSize,
|
|
4927
6172
|
bodyFont,
|
|
4928
6173
|
gridColor: withAlpha(theme.colors.text, 0.12),
|
|
4929
6174
|
axisTextColor: theme.colors.textMuted,
|
|
4930
|
-
entrance,
|
|
6175
|
+
entrance: entrance2,
|
|
4931
6176
|
mark,
|
|
4932
6177
|
labelAnimation
|
|
4933
6178
|
};
|
|
4934
6179
|
const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
|
|
4935
|
-
if (title &&
|
|
6180
|
+
if (title && frame2.titleBand) {
|
|
4936
6181
|
layers.push({
|
|
4937
6182
|
type: "text",
|
|
4938
6183
|
id: "title",
|
|
@@ -4948,9 +6193,9 @@ function renderChart(kind, input, context) {
|
|
|
4948
6193
|
}
|
|
4949
6194
|
},
|
|
4950
6195
|
position: {
|
|
4951
|
-
x:
|
|
4952
|
-
y:
|
|
4953
|
-
width:
|
|
6196
|
+
x: frame2.titleBand.x + frame2.titleBand.width / 2,
|
|
6197
|
+
y: frame2.titleBand.y + frame2.titleBand.height / 2,
|
|
6198
|
+
width: frame2.titleBand.width,
|
|
4954
6199
|
anchor: "center"
|
|
4955
6200
|
},
|
|
4956
6201
|
animation: motionEntrance(context, { type: "fadeIn", duration: 0.8 })
|
|
@@ -4975,11 +6220,11 @@ function renderChart(kind, input, context) {
|
|
|
4975
6220
|
layers.push(...paintScatter(paint));
|
|
4976
6221
|
break;
|
|
4977
6222
|
}
|
|
4978
|
-
if (legend &&
|
|
4979
|
-
layers.push(...paintLegend(paint, legend,
|
|
6223
|
+
if (legend && frame2.legendBand) {
|
|
6224
|
+
layers.push(...paintLegend(paint, legend, frame2.legendBand, legendFontSize));
|
|
4980
6225
|
}
|
|
4981
|
-
if (input.showTable === true &&
|
|
4982
|
-
layers.push(tableLayer(table,
|
|
6226
|
+
if (input.showTable === true && frame2.tableBand) {
|
|
6227
|
+
layers.push(tableLayer(table, frame2.tableBand, tableFontSize, input, context));
|
|
4983
6228
|
}
|
|
4984
6229
|
return layers;
|
|
4985
6230
|
}
|
|
@@ -5060,7 +6305,7 @@ function valueTicks(kind, data, input) {
|
|
|
5060
6305
|
return niceTicks(min, max, 5);
|
|
5061
6306
|
}
|
|
5062
6307
|
function paintValueAxisY(paint, scale, yFor) {
|
|
5063
|
-
const { frame, gridColor, axisTextColor, axisFontSize, bodyFont, input } = paint;
|
|
6308
|
+
const { frame: frame2, gridColor, axisTextColor, axisFontSize, bodyFont, input } = paint;
|
|
5064
6309
|
const layers = [];
|
|
5065
6310
|
scale.ticks.forEach((tick, index) => {
|
|
5066
6311
|
const y = yFor(tick);
|
|
@@ -5072,9 +6317,9 @@ function paintValueAxisY(paint, scale, yFor) {
|
|
|
5072
6317
|
fill: tick === 0 ? withAlpha(paint.context.theme.colors.text, 0.35) : gridColor
|
|
5073
6318
|
},
|
|
5074
6319
|
position: {
|
|
5075
|
-
x:
|
|
6320
|
+
x: frame2.plot.x,
|
|
5076
6321
|
y: y - (tick === 0 ? 1 : 0.5),
|
|
5077
|
-
width:
|
|
6322
|
+
width: frame2.plot.width,
|
|
5078
6323
|
height: tick === 0 ? 2 : 1
|
|
5079
6324
|
}
|
|
5080
6325
|
});
|
|
@@ -5091,7 +6336,7 @@ function paintValueAxisY(paint, scale, yFor) {
|
|
|
5091
6336
|
}
|
|
5092
6337
|
},
|
|
5093
6338
|
position: {
|
|
5094
|
-
x:
|
|
6339
|
+
x: frame2.chartArea.x + (frame2.plot.x - frame2.chartArea.x) / 2,
|
|
5095
6340
|
y,
|
|
5096
6341
|
anchor: "center"
|
|
5097
6342
|
}
|
|
@@ -5100,11 +6345,11 @@ function paintValueAxisY(paint, scale, yFor) {
|
|
|
5100
6345
|
return layers;
|
|
5101
6346
|
}
|
|
5102
6347
|
function paintCategoryAxisX(paint, xForCenter) {
|
|
5103
|
-
const { frame, data, labelFontSize, axisTextColor, bodyFont } = paint;
|
|
5104
|
-
const
|
|
6348
|
+
const { frame: frame2, data, labelFontSize, axisTextColor, bodyFont } = paint;
|
|
6349
|
+
const step2 = categoryLabelStep(data.labels.length, frame2.plot.width, labelFontSize);
|
|
5105
6350
|
const layers = [];
|
|
5106
6351
|
data.labels.forEach((label, index) => {
|
|
5107
|
-
if (index %
|
|
6352
|
+
if (index % step2 !== 0 || !label) return;
|
|
5108
6353
|
layers.push({
|
|
5109
6354
|
type: "text",
|
|
5110
6355
|
id: `cat-${index}`,
|
|
@@ -5119,7 +6364,7 @@ function paintCategoryAxisX(paint, xForCenter) {
|
|
|
5119
6364
|
},
|
|
5120
6365
|
position: {
|
|
5121
6366
|
x: xForCenter(index),
|
|
5122
|
-
y:
|
|
6367
|
+
y: frame2.plot.y + frame2.plot.height + labelFontSize * 1.2,
|
|
5123
6368
|
anchor: "center"
|
|
5124
6369
|
}
|
|
5125
6370
|
});
|
|
@@ -5127,14 +6372,14 @@ function paintCategoryAxisX(paint, xForCenter) {
|
|
|
5127
6372
|
return layers;
|
|
5128
6373
|
}
|
|
5129
6374
|
function paintColumns(paint) {
|
|
5130
|
-
const { frame, data, colors, input, mark, labelAnimation } = paint;
|
|
6375
|
+
const { frame: frame2, data, colors, input, mark, labelAnimation } = paint;
|
|
5131
6376
|
const scale = valueTicks("column", data, input);
|
|
5132
|
-
const yFor = (v) =>
|
|
6377
|
+
const yFor = (v) => frame2.plot.y + frame2.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
|
|
5133
6378
|
const layers = paintValueAxisY(paint, scale, yFor);
|
|
5134
6379
|
const n = data.labels.length;
|
|
5135
|
-
const band =
|
|
6380
|
+
const band = frame2.plot.width / Math.max(1, n);
|
|
5136
6381
|
const innerPad = band * 0.15;
|
|
5137
|
-
layers.push(...paintCategoryAxisX(paint, (i) =>
|
|
6382
|
+
layers.push(...paintCategoryAxisX(paint, (i) => frame2.plot.x + (i + 0.5) * band));
|
|
5138
6383
|
const stacked = input.stacked === true && data.series.length > 1;
|
|
5139
6384
|
const extents = stacked ? stackSeries(data.series) : null;
|
|
5140
6385
|
const groupWidth = band - innerPad * 2;
|
|
@@ -5146,7 +6391,7 @@ function paintColumns(paint) {
|
|
|
5146
6391
|
if (!extent || extent.y0 === extent.y1) return;
|
|
5147
6392
|
const top = yFor(extent.y1);
|
|
5148
6393
|
const height = Math.max(1, yFor(extent.y0) - top);
|
|
5149
|
-
const x = stacked ?
|
|
6394
|
+
const x = stacked ? frame2.plot.x + i * band + innerPad : frame2.plot.x + i * band + innerPad + s * (barWidth + 4);
|
|
5150
6395
|
layers.push({
|
|
5151
6396
|
type: "shape",
|
|
5152
6397
|
id: `mark-${s}-${i}`,
|
|
@@ -5172,7 +6417,7 @@ function paintColumns(paint) {
|
|
|
5172
6417
|
}
|
|
5173
6418
|
function paintBars(paint) {
|
|
5174
6419
|
const {
|
|
5175
|
-
frame,
|
|
6420
|
+
frame: frame2,
|
|
5176
6421
|
data,
|
|
5177
6422
|
colors,
|
|
5178
6423
|
input,
|
|
@@ -5186,11 +6431,11 @@ function paintBars(paint) {
|
|
|
5186
6431
|
} = paint;
|
|
5187
6432
|
const scale = valueTicks("bar", data, input);
|
|
5188
6433
|
const labelGap = axisFontSize * 0.6;
|
|
5189
|
-
const labelWidthPx = (value) =>
|
|
6434
|
+
const labelWidthPx = (value) => formatChartDataValue(value, input.unit).length * axisFontSize * CHART_CHAR_WIDTH_EM;
|
|
5190
6435
|
const showValueLabels = input.showValues === true && !(input.stacked === true);
|
|
5191
6436
|
const reservedForLabels = showValueLabels ? data.series.flatMap((series) => series.values).filter((v) => v !== null).reduce((max, v) => Math.max(max, labelWidthPx(v)), 0) + labelGap : 0;
|
|
5192
|
-
const axisWidth = Math.max(20,
|
|
5193
|
-
const xFor = (v) =>
|
|
6437
|
+
const axisWidth = Math.max(20, frame2.plot.width - reservedForLabels);
|
|
6438
|
+
const xFor = (v) => frame2.plot.x + axisWidth * ((v - scale.niceMin) / (scale.niceMax - scale.niceMin));
|
|
5194
6439
|
const layers = [];
|
|
5195
6440
|
scale.ticks.forEach((tick, index) => {
|
|
5196
6441
|
const x = xFor(tick);
|
|
@@ -5203,9 +6448,9 @@ function paintBars(paint) {
|
|
|
5203
6448
|
},
|
|
5204
6449
|
position: {
|
|
5205
6450
|
x: x - (tick === 0 ? 1 : 0.5),
|
|
5206
|
-
y:
|
|
6451
|
+
y: frame2.plot.y,
|
|
5207
6452
|
width: tick === 0 ? 2 : 1,
|
|
5208
|
-
height:
|
|
6453
|
+
height: frame2.plot.height
|
|
5209
6454
|
}
|
|
5210
6455
|
});
|
|
5211
6456
|
layers.push({
|
|
@@ -5220,11 +6465,11 @@ function paintBars(paint) {
|
|
|
5220
6465
|
textAlign: "center"
|
|
5221
6466
|
}
|
|
5222
6467
|
},
|
|
5223
|
-
position: { x, y:
|
|
6468
|
+
position: { x, y: frame2.plot.y + frame2.plot.height + axisFontSize * 1.2, anchor: "center" }
|
|
5224
6469
|
});
|
|
5225
6470
|
});
|
|
5226
6471
|
const n = data.labels.length;
|
|
5227
|
-
const band =
|
|
6472
|
+
const band = frame2.plot.height / Math.max(1, n);
|
|
5228
6473
|
const innerPad = band * 0.15;
|
|
5229
6474
|
const stacked = input.stacked === true && data.series.length > 1;
|
|
5230
6475
|
const extents = stacked ? stackSeries(data.series) : null;
|
|
@@ -5245,8 +6490,8 @@ function paintBars(paint) {
|
|
|
5245
6490
|
}
|
|
5246
6491
|
},
|
|
5247
6492
|
position: {
|
|
5248
|
-
x:
|
|
5249
|
-
y:
|
|
6493
|
+
x: frame2.chartArea.x + (frame2.plot.x - frame2.chartArea.x) / 2,
|
|
6494
|
+
y: frame2.plot.y + (i + 0.5) * band,
|
|
5250
6495
|
anchor: "center"
|
|
5251
6496
|
}
|
|
5252
6497
|
});
|
|
@@ -5258,7 +6503,7 @@ function paintBars(paint) {
|
|
|
5258
6503
|
if (!extent || extent.y0 === extent.y1) return;
|
|
5259
6504
|
const left = xFor(extent.y0);
|
|
5260
6505
|
const width = Math.max(1, xFor(extent.y1) - left);
|
|
5261
|
-
const y = stacked ?
|
|
6506
|
+
const y = stacked ? frame2.plot.y + i * band + innerPad : frame2.plot.y + i * band + innerPad + s * (barHeight + 4);
|
|
5262
6507
|
layers.push({
|
|
5263
6508
|
type: "shape",
|
|
5264
6509
|
id: `mark-${s}-${i}`,
|
|
@@ -5285,14 +6530,14 @@ function paintBars(paint) {
|
|
|
5285
6530
|
return layers;
|
|
5286
6531
|
}
|
|
5287
6532
|
function paintRadial(paint) {
|
|
5288
|
-
const { kind, frame, data, colors, input, entrance, mark, labelAnimation, context } = paint;
|
|
6533
|
+
const { kind, frame: frame2, data, colors, input, entrance: entrance2, mark, labelAnimation, context } = paint;
|
|
5289
6534
|
const slices = data.labels.map((label, index) => ({ label, value: data.series[0].values[index], index })).filter(
|
|
5290
6535
|
(s) => s.value !== null && Number.isFinite(s.value) && s.value > 0
|
|
5291
6536
|
);
|
|
5292
6537
|
const total = slices.reduce((sum, s) => sum + s.value, 0);
|
|
5293
|
-
const cx =
|
|
5294
|
-
const cy =
|
|
5295
|
-
const rOuter = Math.min(
|
|
6538
|
+
const cx = frame2.chartArea.x + frame2.chartArea.width / 2;
|
|
6539
|
+
const cy = frame2.chartArea.y + frame2.chartArea.height / 2;
|
|
6540
|
+
const rOuter = Math.min(frame2.chartArea.width, frame2.chartArea.height) * 0.42;
|
|
5296
6541
|
const rInner = kind === "donut" ? rOuter * 0.55 : 0;
|
|
5297
6542
|
const layers = [];
|
|
5298
6543
|
let startDeg = 0;
|
|
@@ -5333,7 +6578,7 @@ function paintRadial(paint) {
|
|
|
5333
6578
|
y: cy + rLabel * Math.sin(rad),
|
|
5334
6579
|
anchor: "center"
|
|
5335
6580
|
},
|
|
5336
|
-
animation: labelAnimation(order,
|
|
6581
|
+
animation: labelAnimation(order, entrance2(order))
|
|
5337
6582
|
});
|
|
5338
6583
|
}
|
|
5339
6584
|
startDeg += sweep;
|
|
@@ -5341,12 +6586,12 @@ function paintRadial(paint) {
|
|
|
5341
6586
|
return layers;
|
|
5342
6587
|
}
|
|
5343
6588
|
function paintLines(paint) {
|
|
5344
|
-
const { kind, frame, data, colors, input, mark, labelAnimation } = paint;
|
|
6589
|
+
const { kind, frame: frame2, data, colors, input, mark, labelAnimation } = paint;
|
|
5345
6590
|
const scale = valueTicks(kind, data, input);
|
|
5346
|
-
const yFor = (v) =>
|
|
6591
|
+
const yFor = (v) => frame2.plot.y + frame2.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
|
|
5347
6592
|
const n = data.labels.length;
|
|
5348
|
-
const band =
|
|
5349
|
-
const xForCenter = (i) =>
|
|
6593
|
+
const band = frame2.plot.width / Math.max(1, n);
|
|
6594
|
+
const xForCenter = (i) => frame2.plot.x + (i + 0.5) * band;
|
|
5350
6595
|
const layers = paintValueAxisY(paint, scale, yFor);
|
|
5351
6596
|
layers.push(...paintCategoryAxisX(paint, xForCenter));
|
|
5352
6597
|
const baselineY = yFor(Math.max(scale.niceMin, Math.min(scale.niceMax, 0)));
|
|
@@ -5377,10 +6622,10 @@ function paintLines(paint) {
|
|
|
5377
6622
|
strokeWidth: 0
|
|
5378
6623
|
},
|
|
5379
6624
|
position: {
|
|
5380
|
-
x:
|
|
5381
|
-
y:
|
|
5382
|
-
width:
|
|
5383
|
-
height:
|
|
6625
|
+
x: frame2.plot.x,
|
|
6626
|
+
y: frame2.plot.y,
|
|
6627
|
+
width: frame2.plot.width,
|
|
6628
|
+
height: frame2.plot.height
|
|
5384
6629
|
},
|
|
5385
6630
|
animation: mark(s, "area")
|
|
5386
6631
|
});
|
|
@@ -5391,10 +6636,10 @@ function paintLines(paint) {
|
|
|
5391
6636
|
id: `line-${s}-${seg}`,
|
|
5392
6637
|
content: { d: polylinePath(segment), stroke: colors[s], strokeWidth: 4, fill: "none" },
|
|
5393
6638
|
position: {
|
|
5394
|
-
x:
|
|
5395
|
-
y:
|
|
5396
|
-
width:
|
|
5397
|
-
height:
|
|
6639
|
+
x: frame2.plot.x,
|
|
6640
|
+
y: frame2.plot.y,
|
|
6641
|
+
width: frame2.plot.width,
|
|
6642
|
+
height: frame2.plot.height
|
|
5398
6643
|
},
|
|
5399
6644
|
animation: mark(s, "line")
|
|
5400
6645
|
});
|
|
@@ -5429,9 +6674,9 @@ function paintLines(paint) {
|
|
|
5429
6674
|
return layers;
|
|
5430
6675
|
}
|
|
5431
6676
|
function paintScatter(paint) {
|
|
5432
|
-
const { frame, data, colors, input, mark } = paint;
|
|
6677
|
+
const { frame: frame2, data, colors, input, mark } = paint;
|
|
5433
6678
|
const scale = valueTicks("scatter", data, input);
|
|
5434
|
-
const yFor = (v) =>
|
|
6679
|
+
const yFor = (v) => frame2.plot.y + frame2.plot.height * (1 - (v - scale.niceMin) / (scale.niceMax - scale.niceMin));
|
|
5435
6680
|
const layers = paintValueAxisY(paint, scale, yFor);
|
|
5436
6681
|
const numericCount = data.labelNumbers.filter((v) => v !== null).length;
|
|
5437
6682
|
const numericX = data.labels.length > 0 && numericCount / data.labels.length >= 0.6;
|
|
@@ -5442,15 +6687,15 @@ function paintScatter(paint) {
|
|
|
5442
6687
|
const span = xScale.niceMax - xScale.niceMin;
|
|
5443
6688
|
xFor = (i) => {
|
|
5444
6689
|
const v = data.labelNumbers[i];
|
|
5445
|
-
return v === null ? null :
|
|
6690
|
+
return v === null ? null : frame2.plot.x + frame2.plot.width * ((v - xScale.niceMin) / span);
|
|
5446
6691
|
};
|
|
5447
6692
|
xScale.ticks.forEach((tick, index) => {
|
|
5448
|
-
const x =
|
|
6693
|
+
const x = frame2.plot.x + frame2.plot.width * ((tick - xScale.niceMin) / span);
|
|
5449
6694
|
layers.push({
|
|
5450
6695
|
type: "shape",
|
|
5451
6696
|
id: `xgrid-${index}`,
|
|
5452
6697
|
content: { shape: "rect", fill: paint.gridColor },
|
|
5453
|
-
position: { x: x - 0.5, y:
|
|
6698
|
+
position: { x: x - 0.5, y: frame2.plot.y, width: 1, height: frame2.plot.height }
|
|
5454
6699
|
});
|
|
5455
6700
|
layers.push({
|
|
5456
6701
|
type: "text",
|
|
@@ -5466,15 +6711,15 @@ function paintScatter(paint) {
|
|
|
5466
6711
|
},
|
|
5467
6712
|
position: {
|
|
5468
6713
|
x,
|
|
5469
|
-
y:
|
|
6714
|
+
y: frame2.plot.y + frame2.plot.height + paint.axisFontSize * 1.2,
|
|
5470
6715
|
anchor: "center"
|
|
5471
6716
|
}
|
|
5472
6717
|
});
|
|
5473
6718
|
});
|
|
5474
6719
|
} else {
|
|
5475
|
-
const band =
|
|
5476
|
-
xFor = (i) =>
|
|
5477
|
-
layers.push(...paintCategoryAxisX(paint, (i) =>
|
|
6720
|
+
const band = frame2.plot.width / Math.max(1, data.labels.length);
|
|
6721
|
+
xFor = (i) => frame2.plot.x + (i + 0.5) * band;
|
|
6722
|
+
layers.push(...paintCategoryAxisX(paint, (i) => frame2.plot.x + (i + 0.5) * band));
|
|
5478
6723
|
}
|
|
5479
6724
|
const r = 7;
|
|
5480
6725
|
data.series.forEach((series, s) => {
|
|
@@ -5529,7 +6774,7 @@ function valueLabel(paint, id, value, x, y, animation) {
|
|
|
5529
6774
|
type: "text",
|
|
5530
6775
|
id,
|
|
5531
6776
|
content: {
|
|
5532
|
-
text:
|
|
6777
|
+
text: formatChartDataValue(value, paint.input.unit),
|
|
5533
6778
|
style: {
|
|
5534
6779
|
fontSize: paint.axisFontSize,
|
|
5535
6780
|
fontFamily: paint.bodyFont,
|
|
@@ -5598,19 +6843,22 @@ var TITLE_VARIANTS = {
|
|
|
5598
6843
|
wide: "title-stack",
|
|
5599
6844
|
tall: "split",
|
|
5600
6845
|
square: "title-stack",
|
|
5601
|
-
multiple: "title-stack"
|
|
6846
|
+
multiple: "title-stack",
|
|
6847
|
+
table: "title-stack"
|
|
5602
6848
|
},
|
|
5603
6849
|
square: {
|
|
5604
6850
|
wide: "title-stack",
|
|
5605
6851
|
tall: "split",
|
|
5606
6852
|
square: "title-stack",
|
|
5607
|
-
multiple: "title-stack"
|
|
6853
|
+
multiple: "title-stack",
|
|
6854
|
+
table: "title-stack"
|
|
5608
6855
|
},
|
|
5609
6856
|
portrait: {
|
|
5610
6857
|
wide: "title-stack",
|
|
5611
6858
|
tall: "title-stack",
|
|
5612
6859
|
square: "title-stack",
|
|
5613
|
-
multiple: "title-stack"
|
|
6860
|
+
multiple: "title-stack",
|
|
6861
|
+
table: "title-stack"
|
|
5614
6862
|
}
|
|
5615
6863
|
};
|
|
5616
6864
|
var TEXT_VARIANTS = {
|
|
@@ -5618,19 +6866,22 @@ var TEXT_VARIANTS = {
|
|
|
5618
6866
|
wide: "split",
|
|
5619
6867
|
tall: "split",
|
|
5620
6868
|
square: "split",
|
|
5621
|
-
multiple: "split"
|
|
6869
|
+
multiple: "split",
|
|
6870
|
+
table: "text-stack"
|
|
5622
6871
|
},
|
|
5623
6872
|
square: {
|
|
5624
6873
|
wide: "text-stack",
|
|
5625
6874
|
tall: "split",
|
|
5626
6875
|
square: "text-stack",
|
|
5627
|
-
multiple: "text-stack"
|
|
6876
|
+
multiple: "text-stack",
|
|
6877
|
+
table: "text-stack"
|
|
5628
6878
|
},
|
|
5629
6879
|
portrait: {
|
|
5630
6880
|
wide: "text-stack",
|
|
5631
6881
|
tall: "text-stack",
|
|
5632
6882
|
square: "text-stack",
|
|
5633
|
-
multiple: "text-stack"
|
|
6883
|
+
multiple: "text-stack",
|
|
6884
|
+
table: "text-stack"
|
|
5634
6885
|
}
|
|
5635
6886
|
};
|
|
5636
6887
|
var DENSE_VARIANTS = {
|
|
@@ -5638,19 +6889,22 @@ var DENSE_VARIANTS = {
|
|
|
5638
6889
|
wide: "dense-stack",
|
|
5639
6890
|
tall: "dense-stack",
|
|
5640
6891
|
square: "dense-stack",
|
|
5641
|
-
multiple: "dense-stack"
|
|
6892
|
+
multiple: "dense-stack",
|
|
6893
|
+
table: "dense-stack"
|
|
5642
6894
|
},
|
|
5643
6895
|
square: {
|
|
5644
6896
|
wide: "dense-stack",
|
|
5645
6897
|
tall: "dense-stack",
|
|
5646
6898
|
square: "dense-stack",
|
|
5647
|
-
multiple: "dense-stack"
|
|
6899
|
+
multiple: "dense-stack",
|
|
6900
|
+
table: "dense-stack"
|
|
5648
6901
|
},
|
|
5649
6902
|
portrait: {
|
|
5650
6903
|
wide: "dense-stack",
|
|
5651
6904
|
tall: "dense-stack",
|
|
5652
6905
|
square: "dense-stack",
|
|
5653
|
-
multiple: "dense-stack"
|
|
6906
|
+
multiple: "dense-stack",
|
|
6907
|
+
table: "dense-stack"
|
|
5654
6908
|
}
|
|
5655
6909
|
};
|
|
5656
6910
|
var BLOCK_MEDIA_LAYOUT_POLICIES = {
|
|
@@ -5810,6 +7064,34 @@ var BLOCK_MEDIA_LAYOUT_POLICIES = {
|
|
|
5810
7064
|
unconsumedMedia: "reserve-when-no-native-media",
|
|
5811
7065
|
variants: TEXT_VARIANTS
|
|
5812
7066
|
},
|
|
7067
|
+
cards: {
|
|
7068
|
+
summary: "Text-only cards by default; supplemental media is paired with the cards.",
|
|
7069
|
+
noMedia: "template-default",
|
|
7070
|
+
ownership: "supplemental",
|
|
7071
|
+
unconsumedMedia: "reserved-slot",
|
|
7072
|
+
variants: DENSE_VARIANTS
|
|
7073
|
+
},
|
|
7074
|
+
process: {
|
|
7075
|
+
summary: "A drawn process by default; supplemental media is paired with the stages.",
|
|
7076
|
+
noMedia: "template-default",
|
|
7077
|
+
ownership: "supplemental",
|
|
7078
|
+
unconsumedMedia: "reserved-slot",
|
|
7079
|
+
variants: DENSE_VARIANTS
|
|
7080
|
+
},
|
|
7081
|
+
cycle: {
|
|
7082
|
+
summary: "A drawn cycle by default; supplemental media is paired with the ring.",
|
|
7083
|
+
noMedia: "template-default",
|
|
7084
|
+
ownership: "supplemental",
|
|
7085
|
+
unconsumedMedia: "reserved-slot",
|
|
7086
|
+
variants: TEXT_VARIANTS
|
|
7087
|
+
},
|
|
7088
|
+
layers: {
|
|
7089
|
+
summary: "Drawn concentric layers by default; supplemental media is paired with the rings.",
|
|
7090
|
+
noMedia: "template-default",
|
|
7091
|
+
ownership: "supplemental",
|
|
7092
|
+
unconsumedMedia: "reserved-slot",
|
|
7093
|
+
variants: TEXT_VARIANTS
|
|
7094
|
+
},
|
|
5813
7095
|
photoGrid: {
|
|
5814
7096
|
summary: "Requires a primary image collection and owns the complete tiled media field.",
|
|
5815
7097
|
noMedia: "unsupported",
|
|
@@ -5963,12 +7245,12 @@ function getBlockMediaLayoutPolicy(template) {
|
|
|
5963
7245
|
// src/doc/richMediaLayout.ts
|
|
5964
7246
|
function containedFeatureArea(region, aspects) {
|
|
5965
7247
|
const count = aspects.length;
|
|
5966
|
-
const
|
|
7248
|
+
const columns2 = Math.min(
|
|
5967
7249
|
count,
|
|
5968
7250
|
Math.max(1, Math.ceil(Math.sqrt(count * region.width / region.height)))
|
|
5969
7251
|
);
|
|
5970
|
-
const rows = Math.ceil(count /
|
|
5971
|
-
const width = region.width /
|
|
7252
|
+
const rows = Math.ceil(count / columns2);
|
|
7253
|
+
const width = region.width / columns2;
|
|
5972
7254
|
const height = region.height / rows;
|
|
5973
7255
|
return aspects.reduce((area, aspect) => {
|
|
5974
7256
|
const ratio = aspect && Number.isFinite(aspect) && aspect > 0 ? aspect : 16 / 9;
|
|
@@ -6056,7 +7338,8 @@ function rect(viewport, x, y, width, height) {
|
|
|
6056
7338
|
height: viewport.height * height
|
|
6057
7339
|
};
|
|
6058
7340
|
}
|
|
6059
|
-
function mediaShape(mediaCount, aspectRatios) {
|
|
7341
|
+
function mediaShape(mediaCount, aspectRatios, table = false) {
|
|
7342
|
+
if (table) return "table";
|
|
6060
7343
|
if (mediaCount > 1) return "multiple";
|
|
6061
7344
|
const aspect = aspectRatios[0] ?? 16 / 9;
|
|
6062
7345
|
if (aspect < 0.8) return "tall";
|
|
@@ -6122,6 +7405,7 @@ function overlayMediaRect(viewport, shape) {
|
|
|
6122
7405
|
case "square":
|
|
6123
7406
|
return rect(viewport, 0.49, 0.58, 0.44, 0.35);
|
|
6124
7407
|
case "multiple":
|
|
7408
|
+
case "table":
|
|
6125
7409
|
return rect(viewport, 0.07, 0.67, 0.86, 0.27);
|
|
6126
7410
|
case "wide":
|
|
6127
7411
|
return rect(viewport, 0.07, 0.71, 0.86, 0.23);
|
|
@@ -6134,6 +7418,7 @@ function overlayMediaRect(viewport, shape) {
|
|
|
6134
7418
|
case "square":
|
|
6135
7419
|
return rect(viewport, 0.55, 0.52, 0.4, 0.4);
|
|
6136
7420
|
case "multiple":
|
|
7421
|
+
case "table":
|
|
6137
7422
|
return rect(viewport, 0.08, 0.62, 0.84, 0.31);
|
|
6138
7423
|
case "wide":
|
|
6139
7424
|
return rect(viewport, 0.08, 0.68, 0.84, 0.27);
|
|
@@ -6145,6 +7430,7 @@ function overlayMediaRect(viewport, shape) {
|
|
|
6145
7430
|
case "square":
|
|
6146
7431
|
return rect(viewport, 0.62, 0.22, 0.34, 0.56);
|
|
6147
7432
|
case "multiple":
|
|
7433
|
+
case "table":
|
|
6148
7434
|
return rect(viewport, 0.52, 0.5, 0.44, 0.42);
|
|
6149
7435
|
case "wide":
|
|
6150
7436
|
return rect(viewport, 0.54, 0.58, 0.42, 0.34);
|
|
@@ -6481,14 +7767,14 @@ var MEDIA_BANDS = {
|
|
|
6481
7767
|
"full-frame": "full"
|
|
6482
7768
|
};
|
|
6483
7769
|
var BLOCK_GAP_EM = 0.55;
|
|
6484
|
-
function
|
|
7770
|
+
function textLayer2(layers, id) {
|
|
6485
7771
|
const layer = layers.find((candidate) => candidate.id === id);
|
|
6486
7772
|
return layer?.type === "text" ? layer : void 0;
|
|
6487
7773
|
}
|
|
6488
7774
|
function mediaBandLayout(layers, viewport, band, aspect) {
|
|
6489
7775
|
const backdrop = layers.filter((layer) => isFullViewportBackground(layer, viewport));
|
|
6490
|
-
const title =
|
|
6491
|
-
const body =
|
|
7776
|
+
const title = textLayer2(layers, "title");
|
|
7777
|
+
const body = textLayer2(layers, "body");
|
|
6492
7778
|
if (band === "full" || !title && !body) {
|
|
6493
7779
|
return { layers: backdrop, mediaRect: rect(viewport, 0, 0, 1, 1), framed: false };
|
|
6494
7780
|
}
|
|
@@ -6604,7 +7890,7 @@ function mediaBandLayout(layers, viewport, band, aspect) {
|
|
|
6604
7890
|
}
|
|
6605
7891
|
function resolveSupplementalMediaLayout(layers, template, viewport, mediaCount, aspectRatios, featureOptions = {}) {
|
|
6606
7892
|
const policy = getBlockMediaLayoutPolicy(template);
|
|
6607
|
-
const shape = mediaShape(mediaCount, aspectRatios);
|
|
7893
|
+
const shape = mediaShape(mediaCount, aspectRatios, featureOptions.table);
|
|
6608
7894
|
const alreadyOwnsMedia = layers.some(
|
|
6609
7895
|
(layer) => layer.type === "image" || layer.type === "video" || layer.type === "videoEmbed" || layer.type === "map" || layer.type === "mermaid" || layer.type === "svg"
|
|
6610
7896
|
);
|
|
@@ -6836,14 +8122,14 @@ function planCanvasFrame(template, title, context) {
|
|
|
6836
8122
|
...mediaRect ? { mediaRect } : {}
|
|
6837
8123
|
};
|
|
6838
8124
|
}
|
|
6839
|
-
function placeCanvasGroup(
|
|
6840
|
-
const scale = Math.min(
|
|
8125
|
+
function placeCanvasGroup(frame2, contentW, contentH) {
|
|
8126
|
+
const scale = Math.min(frame2.availW / contentW, frame2.availH / contentH, CANVAS_MAX_SCALE);
|
|
6841
8127
|
const scaledW = contentW * scale;
|
|
6842
8128
|
const scaledH = contentH * scale;
|
|
6843
|
-
const groupH =
|
|
6844
|
-
const groupTop =
|
|
6845
|
-
const offsetX =
|
|
6846
|
-
const offsetY = groupTop +
|
|
8129
|
+
const groupH = frame2.titleBlockPx + scaledH + frame2.descriptionBlockPx;
|
|
8130
|
+
const groupTop = frame2.region.y + Math.max(PADDING / 2, (frame2.region.height - groupH) / 2);
|
|
8131
|
+
const offsetX = frame2.region.x + PADDING + (frame2.availW - scaledW) / 2;
|
|
8132
|
+
const offsetY = groupTop + frame2.titleBlockPx;
|
|
6847
8133
|
return {
|
|
6848
8134
|
scale,
|
|
6849
8135
|
scaledW,
|
|
@@ -6854,8 +8140,8 @@ function placeCanvasGroup(frame, contentW, contentH) {
|
|
|
6854
8140
|
descriptionY: offsetY + scaledH + CANVAS_DESCRIPTION_GAP_PX
|
|
6855
8141
|
};
|
|
6856
8142
|
}
|
|
6857
|
-
function canvasTitleLayer(id, text,
|
|
6858
|
-
const fit =
|
|
8143
|
+
function canvasTitleLayer(id, text, frame2, placement, context) {
|
|
8144
|
+
const fit = frame2.title;
|
|
6859
8145
|
if (!fit) return void 0;
|
|
6860
8146
|
return {
|
|
6861
8147
|
type: "text",
|
|
@@ -6874,15 +8160,15 @@ function canvasTitleLayer(id, text, frame, placement, context) {
|
|
|
6874
8160
|
}
|
|
6875
8161
|
},
|
|
6876
8162
|
position: {
|
|
6877
|
-
x:
|
|
8163
|
+
x: frame2.region.x + PADDING,
|
|
6878
8164
|
y: placement.groupTop,
|
|
6879
8165
|
width: fit.widthPx,
|
|
6880
8166
|
anchor: "top-left"
|
|
6881
8167
|
}
|
|
6882
8168
|
};
|
|
6883
8169
|
}
|
|
6884
|
-
function canvasDescriptionLayer(id,
|
|
6885
|
-
const description =
|
|
8170
|
+
function canvasDescriptionLayer(id, frame2, placement, context) {
|
|
8171
|
+
const description = frame2.description;
|
|
6886
8172
|
if (!description) return void 0;
|
|
6887
8173
|
const { fit, widthPx } = description;
|
|
6888
8174
|
return {
|
|
@@ -6900,7 +8186,7 @@ function canvasDescriptionLayer(id, frame, placement, context) {
|
|
|
6900
8186
|
}
|
|
6901
8187
|
},
|
|
6902
8188
|
position: {
|
|
6903
|
-
x:
|
|
8189
|
+
x: frame2.region.x + (frame2.region.width - widthPx) / 2,
|
|
6904
8190
|
y: placement.descriptionY,
|
|
6905
8191
|
width: widthPx,
|
|
6906
8192
|
anchor: "top-left"
|
|
@@ -7009,10 +8295,10 @@ function diagramBlock(input, context) {
|
|
|
7009
8295
|
const maxX = Math.max(...resolved.nodes.map((n) => n.x + n.w));
|
|
7010
8296
|
const minY = Math.min(...resolved.nodes.map((n) => n.y));
|
|
7011
8297
|
const maxY = Math.max(...resolved.nodes.map((n) => n.y + n.h));
|
|
7012
|
-
const
|
|
8298
|
+
const frame2 = planCanvasFrame("diagram", input.title, context);
|
|
7013
8299
|
const contentW = Math.max(1, maxX - minX);
|
|
7014
8300
|
const contentH = Math.max(1, maxY - minY);
|
|
7015
|
-
const placement = placeCanvasGroup(
|
|
8301
|
+
const placement = placeCanvasGroup(frame2, contentW, contentH);
|
|
7016
8302
|
const { scale, offsetX, offsetY } = placement;
|
|
7017
8303
|
const fontAdj = Math.min(Math.max(scale, 1), 1.5);
|
|
7018
8304
|
const strokeW = Math.round(2 * fontAdj);
|
|
@@ -7024,7 +8310,7 @@ function diagramBlock(input, context) {
|
|
|
7024
8310
|
});
|
|
7025
8311
|
const layers = [...background];
|
|
7026
8312
|
if (input.title) {
|
|
7027
|
-
const titleLayer = canvasTitleLayer("diagram-title", input.title,
|
|
8313
|
+
const titleLayer = canvasTitleLayer("diagram-title", input.title, frame2, placement, context);
|
|
7028
8314
|
if (titleLayer) layers.push(titleLayer);
|
|
7029
8315
|
}
|
|
7030
8316
|
const containers = resolved.nodes.filter((n) => n.isContainer).sort((a, b) => a.depth - b.depth || a.y - b.y || a.x - b.x);
|
|
@@ -7188,7 +8474,7 @@ function diagramBlock(input, context) {
|
|
|
7188
8474
|
};
|
|
7189
8475
|
layers.push(label);
|
|
7190
8476
|
}
|
|
7191
|
-
const descriptionLayer = canvasDescriptionLayer("diagram-description",
|
|
8477
|
+
const descriptionLayer = canvasDescriptionLayer("diagram-description", frame2, placement, context);
|
|
7192
8478
|
if (descriptionLayer) layers.push(descriptionLayer);
|
|
7193
8479
|
return layers;
|
|
7194
8480
|
}
|
|
@@ -7446,8 +8732,8 @@ function timelineBlock(input, context) {
|
|
|
7446
8732
|
const axisWidth = Math.max(1, axisEndX - axisStartX);
|
|
7447
8733
|
const markerRadius = Math.max(6, Math.min(11, Math.min(viewport.width, viewport.height) / 105));
|
|
7448
8734
|
const trackColor = withAlpha(colors.text ?? theme.colors.primary, 0.72);
|
|
7449
|
-
const
|
|
7450
|
-
const branchColor = withAlpha(
|
|
8735
|
+
const accentColor2 = colors.accent ?? theme.colors.primary;
|
|
8736
|
+
const branchColor = withAlpha(accentColor2, 0.78);
|
|
7451
8737
|
const calloutColor = withAlpha(theme.colors.text, 0.48);
|
|
7452
8738
|
const maxCalloutEvents = Math.max(
|
|
7453
8739
|
1,
|
|
@@ -7679,7 +8965,7 @@ function timelineBlock(input, context) {
|
|
|
7679
8965
|
x,
|
|
7680
8966
|
y,
|
|
7681
8967
|
pointMarkerRadius,
|
|
7682
|
-
|
|
8968
|
+
accentColor2,
|
|
7683
8969
|
delay,
|
|
7684
8970
|
build
|
|
7685
8971
|
)
|
|
@@ -7690,8 +8976,8 @@ function timelineBlock(input, context) {
|
|
|
7690
8976
|
id: `timeline-marker-${eventLayerId}`,
|
|
7691
8977
|
content: {
|
|
7692
8978
|
shape: "circle",
|
|
7693
|
-
fill: marker === "hollow" ? theme.colors.background :
|
|
7694
|
-
stroke:
|
|
8979
|
+
fill: marker === "hollow" ? theme.colors.background : accentColor2,
|
|
8980
|
+
stroke: accentColor2,
|
|
7695
8981
|
strokeWidth: Math.max(1.5, pointMarkerRadius * 0.28)
|
|
7696
8982
|
},
|
|
7697
8983
|
position: {
|
|
@@ -8110,17 +9396,17 @@ function drawingBlock(input, context) {
|
|
|
8110
9396
|
const maxX = Math.max(...layout.shapes.map((s) => s.x + s.width));
|
|
8111
9397
|
const minY = Math.min(...layout.shapes.map((s) => s.y));
|
|
8112
9398
|
const maxY = Math.max(...layout.shapes.map((s) => s.y + s.height));
|
|
8113
|
-
const
|
|
9399
|
+
const frame2 = planCanvasFrame("drawing", input.title, context);
|
|
8114
9400
|
const contentW = Math.max(1, maxX - minX);
|
|
8115
9401
|
const contentH = Math.max(1, maxY - minY);
|
|
8116
|
-
const placement = placeCanvasGroup(
|
|
9402
|
+
const placement = placeCanvasGroup(frame2, contentW, contentH);
|
|
8117
9403
|
const { scale, offsetX, offsetY } = placement;
|
|
8118
9404
|
const tx = (x) => offsetX + (x - minX) * scale;
|
|
8119
9405
|
const ty = (y) => offsetY + (y - minY) * scale;
|
|
8120
9406
|
const fontAdj = Math.min(Math.max(scale, 1), 1.5);
|
|
8121
9407
|
const layers = [];
|
|
8122
9408
|
if (input.title) {
|
|
8123
|
-
const titleLayer = canvasTitleLayer("drawing-title", input.title,
|
|
9409
|
+
const titleLayer = canvasTitleLayer("drawing-title", input.title, frame2, placement, context);
|
|
8124
9410
|
if (titleLayer) layers.push(titleLayer);
|
|
8125
9411
|
}
|
|
8126
9412
|
const boxes = /* @__PURE__ */ new Map();
|
|
@@ -8165,7 +9451,7 @@ function drawingBlock(input, context) {
|
|
|
8165
9451
|
const h = s.height * scale;
|
|
8166
9452
|
layers.push(...shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj));
|
|
8167
9453
|
}
|
|
8168
|
-
const descriptionLayer = canvasDescriptionLayer("drawing-description",
|
|
9454
|
+
const descriptionLayer = canvasDescriptionLayer("drawing-description", frame2, placement, context);
|
|
8169
9455
|
if (descriptionLayer) layers.push(descriptionLayer);
|
|
8170
9456
|
return layers;
|
|
8171
9457
|
}
|
|
@@ -8175,7 +9461,7 @@ function shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj
|
|
|
8175
9461
|
const fill = s.fill ?? defaultFill;
|
|
8176
9462
|
if (s.kind === "text") {
|
|
8177
9463
|
out.push(
|
|
8178
|
-
|
|
9464
|
+
textLayer3(
|
|
8179
9465
|
`shape-${s.id}`,
|
|
8180
9466
|
s.text ?? s.label ?? "",
|
|
8181
9467
|
x + w / 2,
|
|
@@ -8234,7 +9520,7 @@ function shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj
|
|
|
8234
9520
|
}
|
|
8235
9521
|
if (s.label) {
|
|
8236
9522
|
out.push(
|
|
8237
|
-
|
|
9523
|
+
textLayer3(
|
|
8238
9524
|
`shape-label-${s.id}`,
|
|
8239
9525
|
s.label,
|
|
8240
9526
|
x + w / 2,
|
|
@@ -8249,7 +9535,7 @@ function shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj
|
|
|
8249
9535
|
}
|
|
8250
9536
|
if (s.sublabel) {
|
|
8251
9537
|
out.push(
|
|
8252
|
-
|
|
9538
|
+
textLayer3(
|
|
8253
9539
|
`shape-sublabel-${s.id}`,
|
|
8254
9540
|
s.sublabel,
|
|
8255
9541
|
x + w / 2,
|
|
@@ -8264,7 +9550,7 @@ function shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj
|
|
|
8264
9550
|
}
|
|
8265
9551
|
return out;
|
|
8266
9552
|
}
|
|
8267
|
-
function
|
|
9553
|
+
function textLayer3(id, text, cx, cy, width, context, color, bold, fontAdj) {
|
|
8268
9554
|
return {
|
|
8269
9555
|
type: "text",
|
|
8270
9556
|
id,
|
|
@@ -8815,6 +10101,10 @@ var templateRegistry = {
|
|
|
8815
10101
|
map: mapBlock,
|
|
8816
10102
|
fullBleedQuote,
|
|
8817
10103
|
list: listBlock,
|
|
10104
|
+
cards: cardsBlock,
|
|
10105
|
+
process: processBlock,
|
|
10106
|
+
cycle: cycleBlock,
|
|
10107
|
+
layers: layersBlock,
|
|
8818
10108
|
photoGrid,
|
|
8819
10109
|
definitionCard,
|
|
8820
10110
|
comparisonBar,
|
|
@@ -9441,6 +10731,278 @@ function extractBodyText(block) {
|
|
|
9441
10731
|
return `${text.slice(0, MAX_BODY_CHARS).trimEnd()}\u2026`;
|
|
9442
10732
|
}
|
|
9443
10733
|
|
|
10734
|
+
// src/doc/structuredData.ts
|
|
10735
|
+
var DATA_FENCE_LANGS = /* @__PURE__ */ new Set(["json", "yaml", "yml"]);
|
|
10736
|
+
function isDataFence(node) {
|
|
10737
|
+
if (!node.lang || !DATA_FENCE_LANGS.has(node.lang.toLowerCase())) return false;
|
|
10738
|
+
const meta = node.meta ?? "";
|
|
10739
|
+
return /\bdata\b/.test(meta);
|
|
10740
|
+
}
|
|
10741
|
+
function parseDataFence(node) {
|
|
10742
|
+
const lang = node.lang?.toLowerCase();
|
|
10743
|
+
if (lang === "json") {
|
|
10744
|
+
try {
|
|
10745
|
+
const parsed = JSON.parse(node.value);
|
|
10746
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10747
|
+
return { error: 'JSON data fence must contain a top-level object ({"key": \u2026})' };
|
|
10748
|
+
}
|
|
10749
|
+
return { data: parsed };
|
|
10750
|
+
} catch (err) {
|
|
10751
|
+
return { error: `invalid JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
10752
|
+
}
|
|
10753
|
+
}
|
|
10754
|
+
try {
|
|
10755
|
+
return { data: parseYamlSubset(node.value) };
|
|
10756
|
+
} catch (err) {
|
|
10757
|
+
return { error: `invalid YAML: ${err instanceof Error ? err.message : String(err)}` };
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
function replaceDataFence(contents, data) {
|
|
10761
|
+
const value = JSON.stringify(data, null, 2);
|
|
10762
|
+
const index = contents.findIndex((node) => node.type === "code" && isDataFence(node));
|
|
10763
|
+
if (index < 0) {
|
|
10764
|
+
const fence = { type: "code", lang: "json", meta: "data", value };
|
|
10765
|
+
return [...contents, fence];
|
|
10766
|
+
}
|
|
10767
|
+
const next = [...contents];
|
|
10768
|
+
next[index] = { ...contents[index], value };
|
|
10769
|
+
return next;
|
|
10770
|
+
}
|
|
10771
|
+
function parseScalar(raw) {
|
|
10772
|
+
const trimmed = raw.trim();
|
|
10773
|
+
if (trimmed.length >= 2) {
|
|
10774
|
+
const q = trimmed[0];
|
|
10775
|
+
if ((q === '"' || q === "'") && trimmed.endsWith(q)) {
|
|
10776
|
+
return trimmed.slice(1, -1);
|
|
10777
|
+
}
|
|
10778
|
+
}
|
|
10779
|
+
if (trimmed === "true") return true;
|
|
10780
|
+
if (trimmed === "false") return false;
|
|
10781
|
+
if (trimmed === "null" || trimmed === "~") return null;
|
|
10782
|
+
if (trimmed !== "" && !isNaN(Number(trimmed))) return Number(trimmed);
|
|
10783
|
+
return trimmed;
|
|
10784
|
+
}
|
|
10785
|
+
function splitInlineArray(interior) {
|
|
10786
|
+
const items = [];
|
|
10787
|
+
let current = "";
|
|
10788
|
+
let quote = null;
|
|
10789
|
+
for (const ch of interior) {
|
|
10790
|
+
if (quote) {
|
|
10791
|
+
current += ch;
|
|
10792
|
+
if (ch === quote) quote = null;
|
|
10793
|
+
} else if (ch === '"' || ch === "'") {
|
|
10794
|
+
quote = ch;
|
|
10795
|
+
current += ch;
|
|
10796
|
+
} else if (ch === ",") {
|
|
10797
|
+
items.push(current);
|
|
10798
|
+
current = "";
|
|
10799
|
+
} else {
|
|
10800
|
+
current += ch;
|
|
10801
|
+
}
|
|
10802
|
+
}
|
|
10803
|
+
items.push(current);
|
|
10804
|
+
return items.map((s) => s.trim()).filter((s) => s !== "");
|
|
10805
|
+
}
|
|
10806
|
+
function parseInlineValue(raw) {
|
|
10807
|
+
const trimmed = raw.trim();
|
|
10808
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
10809
|
+
return splitInlineArray(trimmed.slice(1, -1)).map(parseScalar);
|
|
10810
|
+
}
|
|
10811
|
+
return parseScalar(trimmed);
|
|
10812
|
+
}
|
|
10813
|
+
function parseYamlSubset(src) {
|
|
10814
|
+
const result = {};
|
|
10815
|
+
const lines = src.split("\n");
|
|
10816
|
+
let pendingKey = null;
|
|
10817
|
+
let pendingKind = null;
|
|
10818
|
+
let childIndent = -1;
|
|
10819
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10820
|
+
const line = lines[i];
|
|
10821
|
+
const trimmed = line.trim();
|
|
10822
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
10823
|
+
const indent = line.length - line.trimStart().length;
|
|
10824
|
+
if (indent > 0) {
|
|
10825
|
+
if (!pendingKey) {
|
|
10826
|
+
throw new Error(`line ${i + 1}: unexpected indented line without a preceding "key:" line`);
|
|
10827
|
+
}
|
|
10828
|
+
if (childIndent < 0) childIndent = indent;
|
|
10829
|
+
if (indent > childIndent) {
|
|
10830
|
+
throw new Error(
|
|
10831
|
+
`line ${i + 1}: nesting deeper than one level is not supported \u2014 use a \`\`\`json data fence for nested input`
|
|
10832
|
+
);
|
|
10833
|
+
}
|
|
10834
|
+
if (trimmed.startsWith("- ")) {
|
|
10835
|
+
if (pendingKind === "map") {
|
|
10836
|
+
throw new Error(
|
|
10837
|
+
`line ${i + 1}: cannot mix list items and mapping keys under "${pendingKey}"`
|
|
10838
|
+
);
|
|
10839
|
+
}
|
|
10840
|
+
pendingKind = "list";
|
|
10841
|
+
result[pendingKey].push(parseInlineValue(trimmed.slice(2)));
|
|
10842
|
+
continue;
|
|
10843
|
+
}
|
|
10844
|
+
if (pendingKind === "list") {
|
|
10845
|
+
throw new Error(
|
|
10846
|
+
`line ${i + 1}: cannot mix mapping keys and list items under "${pendingKey}"`
|
|
10847
|
+
);
|
|
10848
|
+
}
|
|
10849
|
+
const subColon = trimmed.indexOf(":");
|
|
10850
|
+
if (subColon < 1) {
|
|
10851
|
+
throw new Error(`line ${i + 1}: expected "key: value"`);
|
|
10852
|
+
}
|
|
10853
|
+
const subValue = trimmed.slice(subColon + 1).trim();
|
|
10854
|
+
if (subValue === "") {
|
|
10855
|
+
throw new Error(
|
|
10856
|
+
`line ${i + 1}: nesting deeper than one level is not supported \u2014 use a \`\`\`json data fence for nested input`
|
|
10857
|
+
);
|
|
10858
|
+
}
|
|
10859
|
+
if (pendingKind !== "map") {
|
|
10860
|
+
pendingKind = "map";
|
|
10861
|
+
result[pendingKey] = {};
|
|
10862
|
+
}
|
|
10863
|
+
result[pendingKey][trimmed.slice(0, subColon).trim()] = parseInlineValue(subValue);
|
|
10864
|
+
continue;
|
|
10865
|
+
}
|
|
10866
|
+
pendingKey = null;
|
|
10867
|
+
pendingKind = null;
|
|
10868
|
+
childIndent = -1;
|
|
10869
|
+
const colonIdx = trimmed.indexOf(":");
|
|
10870
|
+
if (colonIdx < 1) {
|
|
10871
|
+
throw new Error(`line ${i + 1}: expected "key: value"`);
|
|
10872
|
+
}
|
|
10873
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
10874
|
+
const rawValue = trimmed.slice(colonIdx + 1).trim();
|
|
10875
|
+
if (rawValue === "") {
|
|
10876
|
+
result[key] = [];
|
|
10877
|
+
pendingKey = key;
|
|
10878
|
+
} else {
|
|
10879
|
+
result[key] = parseInlineValue(rawValue);
|
|
10880
|
+
}
|
|
10881
|
+
}
|
|
10882
|
+
return result;
|
|
10883
|
+
}
|
|
10884
|
+
function findFirstTable(contents) {
|
|
10885
|
+
return contents?.find((n) => n.type === "table");
|
|
10886
|
+
}
|
|
10887
|
+
function extractTableData(table) {
|
|
10888
|
+
const toCells = (row) => row.children.map((cell) => extractPlainText(cell).trim());
|
|
10889
|
+
const [headerRow, ...bodyRows] = table.children;
|
|
10890
|
+
return {
|
|
10891
|
+
headers: headerRow ? toCells(headerRow) : [],
|
|
10892
|
+
rows: bodyRows.map(toCells),
|
|
10893
|
+
...table.align && table.align.some((a) => a != null) ? { align: table.align } : {}
|
|
10894
|
+
};
|
|
10895
|
+
}
|
|
10896
|
+
|
|
10897
|
+
// src/doc/featureTable.ts
|
|
10898
|
+
var CHARACTER_EM = 0.55;
|
|
10899
|
+
var HEADER_CHARACTER_EM = 0.6;
|
|
10900
|
+
var CELL_SIDE_EM = 1.5;
|
|
10901
|
+
var LINE_EM = 1.2;
|
|
10902
|
+
var CELL_TOP_EM = 1.2;
|
|
10903
|
+
var SAFETY = 0.92;
|
|
10904
|
+
function rowsOf(table) {
|
|
10905
|
+
return [
|
|
10906
|
+
...table.headers.length ? [{ cells: table.headers, em: HEADER_CHARACTER_EM }] : [],
|
|
10907
|
+
...table.rows.map((cells) => ({ cells, em: CHARACTER_EM }))
|
|
10908
|
+
];
|
|
10909
|
+
}
|
|
10910
|
+
function columns(table) {
|
|
10911
|
+
const count = Math.max(1, ...rowsOf(table).map((row) => row.cells.length));
|
|
10912
|
+
return Array.from(
|
|
10913
|
+
{ length: count },
|
|
10914
|
+
(_, index) => rowsOf(table).reduce(
|
|
10915
|
+
(column, row) => {
|
|
10916
|
+
const lines = (row.cells[index] ?? "").split("\n");
|
|
10917
|
+
const words2 = lines.flatMap((line) => line.split(/\s+/u));
|
|
10918
|
+
return {
|
|
10919
|
+
word: Math.max(column.word, ...words2.map((word) => word.length * row.em)),
|
|
10920
|
+
line: Math.max(column.line, ...lines.map((line) => line.trim().length * row.em))
|
|
10921
|
+
};
|
|
10922
|
+
},
|
|
10923
|
+
{ word: 0, line: 0 }
|
|
10924
|
+
)
|
|
10925
|
+
);
|
|
10926
|
+
}
|
|
10927
|
+
function columnWidths(cols, space) {
|
|
10928
|
+
const words2 = cols.reduce((sum, column) => sum + column.word, 0);
|
|
10929
|
+
const lines = cols.reduce((sum, column) => sum + column.line, 0);
|
|
10930
|
+
if (words2 > space) return null;
|
|
10931
|
+
if (lines <= space) return cols.map((column) => column.line);
|
|
10932
|
+
const share = (space - words2) / Math.max(1e-6, lines - words2);
|
|
10933
|
+
return cols.map((column) => column.word + (column.line - column.word) * share);
|
|
10934
|
+
}
|
|
10935
|
+
function tableHeight(table, widths) {
|
|
10936
|
+
return rowsOf(table).reduce((height, row) => {
|
|
10937
|
+
const lines = Math.max(
|
|
10938
|
+
1,
|
|
10939
|
+
...row.cells.map(
|
|
10940
|
+
(cell, index) => cell.split("\n").reduce(
|
|
10941
|
+
(sum, line) => sum + Math.max(1, Math.ceil(line.trim().length * row.em / (widths[index] || 1))),
|
|
10942
|
+
0
|
|
10943
|
+
)
|
|
10944
|
+
)
|
|
10945
|
+
);
|
|
10946
|
+
return height + lines * LINE_EM + CELL_TOP_EM;
|
|
10947
|
+
}, 0);
|
|
10948
|
+
}
|
|
10949
|
+
function featureTableAspect(table) {
|
|
10950
|
+
const cols = columns(table);
|
|
10951
|
+
const widths = cols.map((column) => column.line);
|
|
10952
|
+
const width = widths.reduce((sum, value) => sum + value + CELL_SIDE_EM, 0);
|
|
10953
|
+
return width / Math.max(1, tableHeight(table, widths));
|
|
10954
|
+
}
|
|
10955
|
+
function fitFeatureTable(table, box2, max, min) {
|
|
10956
|
+
const cols = columns(table);
|
|
10957
|
+
const height = (size) => {
|
|
10958
|
+
const widths = columnWidths(cols, box2.width * SAFETY / size - cols.length * CELL_SIDE_EM);
|
|
10959
|
+
return widths ? tableHeight(table, widths) * size + 4 : null;
|
|
10960
|
+
};
|
|
10961
|
+
for (let size = Math.floor(max); size > min; size--) {
|
|
10962
|
+
const needed = height(size);
|
|
10963
|
+
if (needed !== null && needed <= box2.height * SAFETY)
|
|
10964
|
+
return { fontSize: size, height: Math.min(box2.height, needed / SAFETY) };
|
|
10965
|
+
}
|
|
10966
|
+
return { fontSize: min, height: box2.height };
|
|
10967
|
+
}
|
|
10968
|
+
function featureTableLayer(id, table, rect2, context) {
|
|
10969
|
+
const { theme, viewport } = context;
|
|
10970
|
+
const unit = Math.min(viewport.width, viewport.height);
|
|
10971
|
+
const margin = unit * 0.045;
|
|
10972
|
+
const left = Math.max(rect2.x, margin);
|
|
10973
|
+
const box2 = {
|
|
10974
|
+
x: left,
|
|
10975
|
+
y: rect2.y,
|
|
10976
|
+
width: Math.min(rect2.x + rect2.width, viewport.width - margin) - left,
|
|
10977
|
+
height: Math.min(rect2.y + rect2.height, viewport.height - margin) - rect2.y
|
|
10978
|
+
};
|
|
10979
|
+
const { fontSize, height } = fitFeatureTable(table, box2, unit * 0.03, Math.max(8, unit * 8e-3));
|
|
10980
|
+
return {
|
|
10981
|
+
type: "table",
|
|
10982
|
+
id,
|
|
10983
|
+
// As tall as its rows, centered in its box.
|
|
10984
|
+
position: { x: box2.x, y: box2.y + (box2.height - height) / 2, width: box2.width, height },
|
|
10985
|
+
content: {
|
|
10986
|
+
headers: table.headers,
|
|
10987
|
+
rows: table.rows,
|
|
10988
|
+
...table.align ? { align: table.align } : {},
|
|
10989
|
+
wrap: true,
|
|
10990
|
+
style: {
|
|
10991
|
+
// The theme's own color: a summary slide has no color scheme to borrow.
|
|
10992
|
+
headerBackground: theme.colors.primary,
|
|
10993
|
+
headerColor: pickContrastingText(theme.colors.primary),
|
|
10994
|
+
cellBackground: withAlpha(theme.colors.text, 0.04),
|
|
10995
|
+
cellColor: theme.colors.text,
|
|
10996
|
+
borderColor: withAlpha(theme.colors.text, 0.15),
|
|
10997
|
+
fontSize,
|
|
10998
|
+
fontFamily: getThemeFont(context, "body"),
|
|
10999
|
+
headerFontFamily: getThemeFont(context, "title"),
|
|
11000
|
+
borderRadius: 8
|
|
11001
|
+
}
|
|
11002
|
+
}
|
|
11003
|
+
};
|
|
11004
|
+
}
|
|
11005
|
+
|
|
9444
11006
|
// src/doc/materializeBlockLayers.ts
|
|
9445
11007
|
function materializeBlockLayers(block, options = {}) {
|
|
9446
11008
|
const registry = options.customTemplates && options.customTemplates.length > 0 ? buildRegistry(options.customTemplates) : templateRegistry;
|
|
@@ -9566,6 +11128,11 @@ function svgIslandAspectRatio(source) {
|
|
|
9566
11128
|
const result = parseSvgIsland(source);
|
|
9567
11129
|
return result.ok ? result.island.aspectRatio : 16 / 9;
|
|
9568
11130
|
}
|
|
11131
|
+
function featureTables(layers, block) {
|
|
11132
|
+
if (!block.summaryMode || layers.some((layer) => layer.type === "table")) return [];
|
|
11133
|
+
if (block.template && TABLE_FED_TEMPLATES.has(resolveTemplateName(block.template))) return [];
|
|
11134
|
+
return (block.contents ?? []).filter((node) => node.type === "table").map(extractTableData).filter((table) => table.headers.length > 0 || table.rows.length > 0);
|
|
11135
|
+
}
|
|
9569
11136
|
function embeddedSpatialMedia(block) {
|
|
9570
11137
|
const canonicalTemplate = block.template ? resolveTemplateName(block.template) : void 0;
|
|
9571
11138
|
const fences = (block.contents ?? []).filter(
|
|
@@ -9674,6 +11241,11 @@ function collectRichMediaItems(layers, block) {
|
|
|
9674
11241
|
source,
|
|
9675
11242
|
aspectRatio: svgIslandAspectRatio(source)
|
|
9676
11243
|
})),
|
|
11244
|
+
...featureTables(layers, block).map((table) => ({
|
|
11245
|
+
kind: "table",
|
|
11246
|
+
table,
|
|
11247
|
+
aspectRatio: featureTableAspect(table)
|
|
11248
|
+
})),
|
|
9677
11249
|
...embeddedSpatialMedia(block)
|
|
9678
11250
|
];
|
|
9679
11251
|
}
|
|
@@ -9687,16 +11259,16 @@ function gridCells(region, count, viewport, inset) {
|
|
|
9687
11259
|
width: Math.max(0, region.width - padding * 2),
|
|
9688
11260
|
height: Math.max(0, region.height - padding * 2)
|
|
9689
11261
|
};
|
|
9690
|
-
const
|
|
11262
|
+
const columns2 = Math.min(
|
|
9691
11263
|
count,
|
|
9692
11264
|
Math.max(1, Math.ceil(Math.sqrt(count * (inner.width / Math.max(1, inner.height)))))
|
|
9693
11265
|
);
|
|
9694
|
-
const rows = Math.ceil(count /
|
|
9695
|
-
const width = (inner.width - gap * (
|
|
11266
|
+
const rows = Math.ceil(count / columns2);
|
|
11267
|
+
const width = (inner.width - gap * (columns2 - 1)) / columns2;
|
|
9696
11268
|
const height = (inner.height - gap * (rows - 1)) / rows;
|
|
9697
11269
|
return Array.from({ length: count }, (_, index) => ({
|
|
9698
|
-
x: inner.x + index %
|
|
9699
|
-
y: inner.y + Math.floor(index /
|
|
11270
|
+
x: inner.x + index % columns2 * (width + gap),
|
|
11271
|
+
y: inner.y + Math.floor(index / columns2) * (height + gap),
|
|
9700
11272
|
width,
|
|
9701
11273
|
height
|
|
9702
11274
|
}));
|
|
@@ -9708,7 +11280,7 @@ function effectiveImageTreatment(theme, block) {
|
|
|
9708
11280
|
if (!base || base.type === "none") return void 0;
|
|
9709
11281
|
return base.type === "duotone" && !base.color ? { ...base, color: theme.colors.primary } : base;
|
|
9710
11282
|
}
|
|
9711
|
-
function richMediaLayers(item, position, block, theme, kindIndex) {
|
|
11283
|
+
function richMediaLayers(item, position, block, theme, viewport, kindIndex) {
|
|
9712
11284
|
if (item.kind === "image") {
|
|
9713
11285
|
const treatment = effectiveImageTreatment(theme, block);
|
|
9714
11286
|
return [
|
|
@@ -9780,6 +11352,16 @@ function richMediaLayers(item, position, block, theme, kindIndex) {
|
|
|
9780
11352
|
}
|
|
9781
11353
|
];
|
|
9782
11354
|
}
|
|
11355
|
+
if (item.kind === "table") {
|
|
11356
|
+
return [
|
|
11357
|
+
featureTableLayer(
|
|
11358
|
+
`${block.id}-table-${kindIndex}`,
|
|
11359
|
+
item.table,
|
|
11360
|
+
position,
|
|
11361
|
+
createTemplateContext(theme, 0, 1, viewport)
|
|
11362
|
+
)
|
|
11363
|
+
];
|
|
11364
|
+
}
|
|
9783
11365
|
const localViewport = {
|
|
9784
11366
|
width: position.width,
|
|
9785
11367
|
height: position.height,
|
|
@@ -9833,7 +11415,7 @@ function appendRichContentLayers(layers, block, theme, viewport, mediaAspectRati
|
|
|
9833
11415
|
const key = item.kind === "image" || item.kind === "video" ? item.src : item.kind === "mermaid" ? item.source : void 0;
|
|
9834
11416
|
const intrinsic = key ? mediaAspectRatios?.[key] : void 0;
|
|
9835
11417
|
if (intrinsic && Number.isFinite(intrinsic) && intrinsic > 0) return intrinsic;
|
|
9836
|
-
return item.kind === "video" ? void 0 : item.aspectRatio;
|
|
11418
|
+
return item.kind === "video" || item.kind === "table" ? void 0 : item.aspectRatio;
|
|
9837
11419
|
});
|
|
9838
11420
|
const aspects = items.map((item, index) => knownAspects[index] ?? item.aspectRatio);
|
|
9839
11421
|
const layout = block.summaryLayout === "feature" ? resolveFeatureSummaryLayout(timedLayers, viewport, aspects) : resolveSupplementalMediaLayout(
|
|
@@ -9845,7 +11427,8 @@ function appendRichContentLayers(layers, block, theme, viewport, mediaAspectRati
|
|
|
9845
11427
|
{
|
|
9846
11428
|
summaryFeature: block.summaryMode === "headings-and-features" && !timedLayers.some((layer) => layer.id === "feature-body"),
|
|
9847
11429
|
stackColumns: createTemplateContext(theme, 0, 1, viewport).layout.stackColumns,
|
|
9848
|
-
knownAspectRatios: knownAspects
|
|
11430
|
+
knownAspectRatios: knownAspects,
|
|
11431
|
+
table: items.some((item) => item.kind === "table")
|
|
9849
11432
|
}
|
|
9850
11433
|
);
|
|
9851
11434
|
const cells = gridCells(layout.mediaRect, items.length, viewport, layout.framed);
|
|
@@ -9855,13 +11438,14 @@ function appendRichContentLayers(layers, block, theme, viewport, mediaAspectRati
|
|
|
9855
11438
|
videoEmbed: 0,
|
|
9856
11439
|
mermaid: 0,
|
|
9857
11440
|
svg: 0,
|
|
11441
|
+
table: 0,
|
|
9858
11442
|
spatial: 0
|
|
9859
11443
|
};
|
|
9860
11444
|
const mediaLayers = items.flatMap((item, index) => {
|
|
9861
11445
|
counters[item.kind] += 1;
|
|
9862
|
-
return richMediaLayers(item, cells[index], block, theme, counters[item.kind]);
|
|
11446
|
+
return richMediaLayers(item, cells[index], block, theme, viewport, counters[item.kind]);
|
|
9863
11447
|
});
|
|
9864
|
-
const
|
|
11448
|
+
const frame2 = layout.framed ? {
|
|
9865
11449
|
id: `${block.id}-rich-media-frame`,
|
|
9866
11450
|
type: "shape",
|
|
9867
11451
|
position: layout.mediaRect,
|
|
@@ -9873,7 +11457,7 @@ function appendRichContentLayers(layers, block, theme, viewport, mediaAspectRati
|
|
|
9873
11457
|
borderRadius: Math.max(12, Math.round(Math.min(viewport.width, viewport.height) * 0.018))
|
|
9874
11458
|
}
|
|
9875
11459
|
} : void 0;
|
|
9876
|
-
return [...layout.layers, ...
|
|
11460
|
+
return [...layout.layers, ...frame2 ? [frame2] : [], ...mediaLayers];
|
|
9877
11461
|
}
|
|
9878
11462
|
function executeTemplateMaterialization(templateBlock, context, registry) {
|
|
9879
11463
|
const safeTemplateBlock = cloneData(templateBlock);
|
|
@@ -10031,6 +11615,22 @@ var TEMPLATE_METADATA = {
|
|
|
10031
11615
|
label: "List",
|
|
10032
11616
|
description: "Renders a bulleted or numbered list in a clean, card-style layout."
|
|
10033
11617
|
},
|
|
11618
|
+
cards: {
|
|
11619
|
+
label: "Cards",
|
|
11620
|
+
description: "Shows two to six parallel ideas side by side as cards, each revealed in turn."
|
|
11621
|
+
},
|
|
11622
|
+
process: {
|
|
11623
|
+
label: "Process",
|
|
11624
|
+
description: "Shows an ordered process as numbered stages joined by arrows."
|
|
11625
|
+
},
|
|
11626
|
+
cycle: {
|
|
11627
|
+
label: "Cycle",
|
|
11628
|
+
description: "Arranges the stages of a cycle on a ring, the last leading back to the first."
|
|
11629
|
+
},
|
|
11630
|
+
layers: {
|
|
11631
|
+
label: "Layers",
|
|
11632
|
+
description: "Draws nested parts as concentric rings, innermost first, each one called out."
|
|
11633
|
+
},
|
|
10034
11634
|
photoGrid: {
|
|
10035
11635
|
label: "Photo Grid",
|
|
10036
11636
|
description: "Arranges up to four photos in a mosaic grid."
|
|
@@ -10220,6 +11820,30 @@ var TEMPLATE_AUTHORING_METADATA = {
|
|
|
10220
11820
|
safeForContentFirst: false,
|
|
10221
11821
|
placement: "heading"
|
|
10222
11822
|
},
|
|
11823
|
+
cards: {
|
|
11824
|
+
role: "content",
|
|
11825
|
+
bodyPolicy: "derived",
|
|
11826
|
+
safeForContentFirst: false,
|
|
11827
|
+
placement: "heading"
|
|
11828
|
+
},
|
|
11829
|
+
process: {
|
|
11830
|
+
role: "content",
|
|
11831
|
+
bodyPolicy: "derived",
|
|
11832
|
+
safeForContentFirst: false,
|
|
11833
|
+
placement: "heading"
|
|
11834
|
+
},
|
|
11835
|
+
cycle: {
|
|
11836
|
+
role: "content",
|
|
11837
|
+
bodyPolicy: "derived",
|
|
11838
|
+
safeForContentFirst: false,
|
|
11839
|
+
placement: "heading"
|
|
11840
|
+
},
|
|
11841
|
+
layers: {
|
|
11842
|
+
role: "content",
|
|
11843
|
+
bodyPolicy: "derived",
|
|
11844
|
+
safeForContentFirst: false,
|
|
11845
|
+
placement: "heading"
|
|
11846
|
+
},
|
|
10223
11847
|
photoGrid: {
|
|
10224
11848
|
role: "media",
|
|
10225
11849
|
bodyPolicy: "structured",
|
|
@@ -10336,6 +11960,177 @@ var TEMPLATE_AUTHORING_METADATA = {
|
|
|
10336
11960
|
}
|
|
10337
11961
|
};
|
|
10338
11962
|
|
|
11963
|
+
// src/doc/templates/authoringGuide.ts
|
|
11964
|
+
var CHART = {
|
|
11965
|
+
fields: "title, headers [label, value], rows [[label, value], \u2026] 2\u20136 rows, showValues: true",
|
|
11966
|
+
use: "only values the {source} states"
|
|
11967
|
+
};
|
|
11968
|
+
var CONCEPT_BUILD = 'build: {mode: "items"} to reveal them as they are spoken';
|
|
11969
|
+
var TEMPLATE_GUIDES = {
|
|
11970
|
+
imageWithCaption: {
|
|
11971
|
+
fields: "imageSrc, imageAlt, caption \u2264 90 chars, ambientMotion: zoomIn|zoomOut|panLeft|panRight",
|
|
11972
|
+
use: "a photograph with the narration's idea as caption"
|
|
11973
|
+
},
|
|
11974
|
+
pullQuote: {
|
|
11975
|
+
fields: "text, backgroundImage {src, alt}, ambientMotion",
|
|
11976
|
+
use: "a VERBATIM sentence or clause from the {source} over a photograph"
|
|
11977
|
+
},
|
|
11978
|
+
fullBleedQuote: {
|
|
11979
|
+
fields: "text",
|
|
11980
|
+
use: "a VERBATIM short sentence from the {source}, text only"
|
|
11981
|
+
},
|
|
11982
|
+
factCard: {
|
|
11983
|
+
fields: "fact \u2264 4 words, explanation \u2264 160 chars, accentImage {src, alt, position: left-strip|right-strip}"
|
|
11984
|
+
},
|
|
11985
|
+
dateEvent: {
|
|
11986
|
+
fields: "date, description \u2264 160 chars, mood: neutral|somber|celebratory, accentImage optional",
|
|
11987
|
+
use: "a year or period the {source} states"
|
|
11988
|
+
},
|
|
11989
|
+
statHighlight: {
|
|
11990
|
+
fields: 'stat like "98%" or "36", description \u2264 60 chars, detail \u2264 80 chars, accentImage optional',
|
|
11991
|
+
use: "counts up; the number must be in the {source}"
|
|
11992
|
+
},
|
|
11993
|
+
comparisonBar: {
|
|
11994
|
+
fields: "leftLabel, leftValue, rightLabel, rightValue, unit optional",
|
|
11995
|
+
use: "two quantities the {source} gives in the same unit"
|
|
11996
|
+
},
|
|
11997
|
+
columnChart: CHART,
|
|
11998
|
+
barChart: CHART,
|
|
11999
|
+
pieChart: CHART,
|
|
12000
|
+
timeline: {
|
|
12001
|
+
fields: "title, tracks: [{ id, label, events: [{ id, label: year, position: 0..1 in date order, description \u2264 40 chars, marker: 'diamond' optional }] }]",
|
|
12002
|
+
use: "3\u20137 dated events the {source} states"
|
|
12003
|
+
},
|
|
12004
|
+
diagram: {
|
|
12005
|
+
fields: "title, nodeShape: 'pill', nodes: [{ id, label \u2264 3 words, x, y }] 3\u20136 nodes with x in {0, 280, 560} and y in {0, 160, 320}, edges: [{ source, target, label \u2264 3 words }]",
|
|
12006
|
+
use: "relationships the {source} describes"
|
|
12007
|
+
},
|
|
12008
|
+
list: {
|
|
12009
|
+
fields: "title, items: 2\u20135 short items taken from the {source}'s own words"
|
|
12010
|
+
},
|
|
12011
|
+
photoGrid: {
|
|
12012
|
+
fields: "images: [{src, alt}] \xD7 2\u20134, caption \u2264 90 chars",
|
|
12013
|
+
use: "several related photographs"
|
|
12014
|
+
},
|
|
12015
|
+
map: {
|
|
12016
|
+
fields: "center: {lat, lng}, zoom, mapStyle: 'terrain', title, ambientMotion",
|
|
12017
|
+
use: "where the place is, at its first mention"
|
|
12018
|
+
},
|
|
12019
|
+
sectionHeader: {
|
|
12020
|
+
fields: "title = the section title, imageSrc, imageAlt, ambientMotion",
|
|
12021
|
+
use: "opens a section on a photograph"
|
|
12022
|
+
},
|
|
12023
|
+
cards: {
|
|
12024
|
+
fields: `title, items: ["Label: detail", \u2026] 2\u20134, versus: true for exactly two set against each other, ${CONCEPT_BUILD}`,
|
|
12025
|
+
use: "parallel ideas side by side, such as kinds, causes or features"
|
|
12026
|
+
},
|
|
12027
|
+
process: {
|
|
12028
|
+
fields: `title, items: ["Label: detail", \u2026] 2\u20134 in order, ${CONCEPT_BUILD}`,
|
|
12029
|
+
use: "ordered stages or dated events, drawn as numbered steps joined by arrows"
|
|
12030
|
+
},
|
|
12031
|
+
cycle: {
|
|
12032
|
+
fields: `title, items: ["Label: detail", \u2026] 3\u20135, ${CONCEPT_BUILD}`,
|
|
12033
|
+
use: "stages that repeat in a loop, drawn on a ring"
|
|
12034
|
+
},
|
|
12035
|
+
layers: {
|
|
12036
|
+
fields: `title, items: ["Label: detail", \u2026] 3\u20135 innermost first, ${CONCEPT_BUILD}`,
|
|
12037
|
+
use: "parts nested inside one another, drawn as concentric rings"
|
|
12038
|
+
}
|
|
12039
|
+
};
|
|
12040
|
+
function guideText(text, wording = {}) {
|
|
12041
|
+
return text.replace(/\{source\}/gu, wording.source ?? "source");
|
|
12042
|
+
}
|
|
12043
|
+
function templateCatalog(entries, wording = {}) {
|
|
12044
|
+
return entries.map((entry) => {
|
|
12045
|
+
const [name, guide] = typeof entry === "string" ? [entry, TEMPLATE_GUIDES[entry]] : "templates" in entry ? [entry.templates.join(" | "), TEMPLATE_GUIDES[entry.templates[0]]] : [entry.template, entry];
|
|
12046
|
+
const use = guide.use ? ` \u2014 ${guide.use}` : "";
|
|
12047
|
+
return guideText(`- ${name} (${guide.fields})${use}`, wording);
|
|
12048
|
+
});
|
|
12049
|
+
}
|
|
12050
|
+
var PRESENTATION_LAYOUT_GUIDE = {
|
|
12051
|
+
title: "an opening",
|
|
12052
|
+
divider: "a new topic begins; only the headline shows",
|
|
12053
|
+
statement: "one idea",
|
|
12054
|
+
emphasis: "one striking takeaway; only the headline shows, so keep it under 60 characters",
|
|
12055
|
+
quote: "the headline is one sentence copied word for word from the passage, without quotation marks; only the headline shows",
|
|
12056
|
+
stat: "the passage\u2019s key figure, such as 40% or $3 million, stated in the headline or a point",
|
|
12057
|
+
definition: "the headline is a term and the points define it",
|
|
12058
|
+
list: "2\u20133 points",
|
|
12059
|
+
comparison: "exactly 2 genuinely contrasting points",
|
|
12060
|
+
steps: "2\u20133 ordered actions",
|
|
12061
|
+
image: 'only when the current layout is "image"; the headline is then its caption',
|
|
12062
|
+
cards: "2\u20134 parallel ideas side by side, such as kinds, causes or features",
|
|
12063
|
+
process: "2\u20134 ordered stages or dated events, drawn as numbered steps joined by arrows",
|
|
12064
|
+
cycle: "3\u20135 stages that repeat in a loop, drawn on a ring",
|
|
12065
|
+
layers: "3\u20135 parts nested inside one another, innermost first, such as a core and the layers around it, drawn as concentric rings"
|
|
12066
|
+
};
|
|
12067
|
+
var DRAWN = ["cards", "process", "cycle", "layers"];
|
|
12068
|
+
var NUMBERS = ["statHighlight", "comparisonBar", "columnChart", "barChart", "pieChart"];
|
|
12069
|
+
var ACCENTED = [
|
|
12070
|
+
"factCard",
|
|
12071
|
+
"statHighlight",
|
|
12072
|
+
"dateEvent",
|
|
12073
|
+
"quote",
|
|
12074
|
+
"list",
|
|
12075
|
+
"definitionCard"
|
|
12076
|
+
];
|
|
12077
|
+
var AUTHORING_TRICKS = [
|
|
12078
|
+
{
|
|
12079
|
+
id: "one-idea",
|
|
12080
|
+
text: "One idea per slide: each slide shows what the sentence being spoken is about. Captions and labels restate its idea briefly; they never add a fact."
|
|
12081
|
+
},
|
|
12082
|
+
{
|
|
12083
|
+
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.",
|
|
12085
|
+
templates: [...NUMBERS, "timeline"]
|
|
12086
|
+
},
|
|
12087
|
+
{
|
|
12088
|
+
id: "ranges",
|
|
12089
|
+
text: 'A range ("3 to 7 miles deep") is not one value: never chart it as a single bar or count up to one end. Write it as the {source} gives it, on a card or in a list.',
|
|
12090
|
+
templates: NUMBERS
|
|
12091
|
+
},
|
|
12092
|
+
{
|
|
12093
|
+
id: "draw-ideas",
|
|
12094
|
+
text: `Slides that draw their ideas explain best: several parallel ideas \u2192 cards, ordered stages \u2192 process, a loop that comes back to its start \u2192 cycle, parts nested inside one another \u2192 layers; prefer them to a list. Write each item as "Label: detail", a label of 1\u20134 words and a detail of at most 70 characters, in the {source}'s own words. For dated events, start each label with its date: a process of dated stages draws as a timeline.`,
|
|
12095
|
+
summary: `Slides that draw their ideas explain best: when the passage has several parallel ideas, ordered stages, a repeating loop or nested parts, choose ${DRAWN.map((layout) => `"${layout}"`).join(", ")} rather than "statement" or "list", and write each of their points (up to 4) as "Label: detail", a label of 1\u20134 words and a detail of at most 70 characters. For dated events, start each label with its date.`,
|
|
12096
|
+
templates: DRAWN
|
|
12097
|
+
},
|
|
12098
|
+
{
|
|
12099
|
+
id: "reveal-as-spoken",
|
|
12100
|
+
text: 'A drawn slide can reveal its items one at a time as the narration says them (build {mode: "items"}). Put the items in the order they are spoken and let each share its key words with the sentence that says it, so every reveal lands on its words.',
|
|
12101
|
+
templates: DRAWN
|
|
12102
|
+
},
|
|
12103
|
+
{
|
|
12104
|
+
id: "chain-not-diagram",
|
|
12105
|
+
text: "A chain \u2014 one thing leads to the next, which leads to the next \u2014 is a process, not a diagram. Keep diagrams for relationships that branch or meet: causes on the left and effects on the right, every edge labelled with the verb the {source} uses, and no edges that cross.",
|
|
12106
|
+
templates: ["diagram", "process"]
|
|
12107
|
+
},
|
|
12108
|
+
{
|
|
12109
|
+
id: "no-empty-frames",
|
|
12110
|
+
text: "A text card on its own is small type on an empty field. Give each factCard, statHighlight, dateEvent, quote, list and definitionCard an accentImage showing what its sentence is about, and alternate the side from card to card.",
|
|
12111
|
+
templates: ACCENTED
|
|
12112
|
+
},
|
|
12113
|
+
{
|
|
12114
|
+
id: "motion-follows-picture",
|
|
12115
|
+
text: "Motion comes from the theme's motion profile; never hand-author animations. On a photograph, choose the ambientMotion the picture suggests \u2014 zoomIn toward a detail, panLeft or panRight along a skyline or coast \u2014 and alternate directions between neighbouring slides.",
|
|
12116
|
+
templates: ["imageWithCaption", "pullQuote", "sectionHeader", "map"]
|
|
12117
|
+
},
|
|
12118
|
+
{
|
|
12119
|
+
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."
|
|
12121
|
+
}
|
|
12122
|
+
];
|
|
12123
|
+
function authoringTricks(options = {}) {
|
|
12124
|
+
const offered = options.templates ? new Set(options.templates) : void 0;
|
|
12125
|
+
const selected = options.ids ? options.ids.flatMap((id) => AUTHORING_TRICKS.filter((trick) => trick.id === id)) : AUTHORING_TRICKS;
|
|
12126
|
+
return selected.filter(
|
|
12127
|
+
(trick) => !offered || !trick.templates || trick.templates.some((template) => offered.has(template))
|
|
12128
|
+
).flatMap((trick) => {
|
|
12129
|
+
const text = options.surface === "summary" ? trick.summary : trick.text;
|
|
12130
|
+
return text ? [guideText(text, options.wording)] : [];
|
|
12131
|
+
});
|
|
12132
|
+
}
|
|
12133
|
+
|
|
10339
12134
|
// src/doc/templates/coverBlock.ts
|
|
10340
12135
|
var COVER_TITLE_BASE_PX = 120;
|
|
10341
12136
|
var COVER_TITLE_COMFORTABLE_CHARS = 26;
|
|
@@ -10430,7 +12225,7 @@ function frostedCoverLayout(title, subtitle, context, coverLayout = "stacked") {
|
|
|
10430
12225
|
};
|
|
10431
12226
|
};
|
|
10432
12227
|
if (coverLayout === "split" && subtitle?.trim()) {
|
|
10433
|
-
const { fontSize: fontSize2, lines: lines2, textWidth: textWidth2, textHeight: textHeight2, padX: padX2, padY: padY2, radius: radius2, box:
|
|
12228
|
+
const { fontSize: fontSize2, lines: lines2, textWidth: textWidth2, textHeight: textHeight2, padX: padX2, padY: padY2, radius: radius2, box: box3 } = fitTitle(FROSTED_SPLIT_TITLE_SCALE);
|
|
10434
12229
|
const deckMin = themedFontSize(COVER_SUBTITLE_BASE_PX, context, false);
|
|
10435
12230
|
const deck = fitFrostedText(
|
|
10436
12231
|
subtitle.trim(),
|
|
@@ -10464,7 +12259,7 @@ function frostedCoverLayout(title, subtitle, context, coverLayout = "stacked") {
|
|
|
10464
12259
|
fontSize: fontSize2,
|
|
10465
12260
|
lines: lines2,
|
|
10466
12261
|
plate: titlePlate,
|
|
10467
|
-
title:
|
|
12262
|
+
title: box3(titlePlate.y),
|
|
10468
12263
|
subtitle: {
|
|
10469
12264
|
cx,
|
|
10470
12265
|
cy: deckPlate.y + deckHeight / 2,
|
|
@@ -10480,7 +12275,7 @@ function frostedCoverLayout(title, subtitle, context, coverLayout = "stacked") {
|
|
|
10480
12275
|
};
|
|
10481
12276
|
}
|
|
10482
12277
|
}
|
|
10483
|
-
const { fontSize, lines, column, textWidth, textHeight, padX, padY, radius, box } = fitTitle(1);
|
|
12278
|
+
const { fontSize, lines, column, textWidth, textHeight, padX, padY, radius, box: box2 } = fitTitle(1);
|
|
10484
12279
|
let sub;
|
|
10485
12280
|
if (subtitle?.trim()) {
|
|
10486
12281
|
const subSize = themedFontSize(COVER_SUBTITLE_BASE_PX, context, false);
|
|
@@ -10509,7 +12304,7 @@ function frostedCoverLayout(title, subtitle, context, coverLayout = "stacked") {
|
|
|
10509
12304
|
height: plateHeight,
|
|
10510
12305
|
radius
|
|
10511
12306
|
},
|
|
10512
|
-
title:
|
|
12307
|
+
title: box2(plateY),
|
|
10513
12308
|
...sub ? {
|
|
10514
12309
|
subtitle: {
|
|
10515
12310
|
cx,
|
|
@@ -10948,9 +12743,9 @@ function expandDocBlocks(blocks, options = {}) {
|
|
|
10948
12743
|
return expandedBlock;
|
|
10949
12744
|
});
|
|
10950
12745
|
}
|
|
10951
|
-
const
|
|
10952
|
-
|
|
10953
|
-
|
|
12746
|
+
const buildSegments = new Set(
|
|
12747
|
+
blocks.filter(hasBlockBuilds).map((block) => block.audioSegment ?? 0)
|
|
12748
|
+
);
|
|
10954
12749
|
const blocksBySegment = /* @__PURE__ */ new Map();
|
|
10955
12750
|
blocks.forEach((block, index) => {
|
|
10956
12751
|
const segmentIndex = block.audioSegment ?? 0;
|
|
@@ -10963,6 +12758,8 @@ function expandDocBlocks(blocks, options = {}) {
|
|
|
10963
12758
|
const splitParts = /* @__PURE__ */ new Map();
|
|
10964
12759
|
for (const [segmentIndex, segmentBlocks] of blocksBySegment) {
|
|
10965
12760
|
const audioSegment = audioSegments[segmentIndex];
|
|
12761
|
+
const splitLongBlocks = requestedSplit && !buildSegments.has(segmentIndex);
|
|
12762
|
+
const mergeShortBlocks = requestedMerge && !buildSegments.has(segmentIndex);
|
|
10966
12763
|
if (segmentBlocks.every(({ block }) => block.timelineLocked === true)) {
|
|
10967
12764
|
for (const { block, originalIndex } of segmentBlocks) {
|
|
10968
12765
|
const expanded = materializeScheduledBlock(
|
|
@@ -11179,7 +12976,9 @@ function expandDocBlocks(blocks, options = {}) {
|
|
|
11179
12976
|
}
|
|
11180
12977
|
expandedBlocks.splice(0, expandedBlocks.length, ...sequenced);
|
|
11181
12978
|
}
|
|
11182
|
-
return
|
|
12979
|
+
return requestedMerge ? expandedBlocks.filter(
|
|
12980
|
+
(block) => block && (block.duration > 0 || buildSegments.has(block.audioSegment ?? 0))
|
|
12981
|
+
) : expandedBlocks.filter(Boolean);
|
|
11183
12982
|
}
|
|
11184
12983
|
function getAvailableTemplates() {
|
|
11185
12984
|
return Object.keys(templateRegistry);
|
|
@@ -11188,169 +12987,6 @@ function hasTemplate(name) {
|
|
|
11188
12987
|
return resolveTemplateName(name) in templateRegistry;
|
|
11189
12988
|
}
|
|
11190
12989
|
|
|
11191
|
-
// src/doc/structuredData.ts
|
|
11192
|
-
var DATA_FENCE_LANGS = /* @__PURE__ */ new Set(["json", "yaml", "yml"]);
|
|
11193
|
-
function isDataFence(node) {
|
|
11194
|
-
if (!node.lang || !DATA_FENCE_LANGS.has(node.lang.toLowerCase())) return false;
|
|
11195
|
-
const meta = node.meta ?? "";
|
|
11196
|
-
return /\bdata\b/.test(meta);
|
|
11197
|
-
}
|
|
11198
|
-
function parseDataFence(node) {
|
|
11199
|
-
const lang = node.lang?.toLowerCase();
|
|
11200
|
-
if (lang === "json") {
|
|
11201
|
-
try {
|
|
11202
|
-
const parsed = JSON.parse(node.value);
|
|
11203
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11204
|
-
return { error: 'JSON data fence must contain a top-level object ({"key": \u2026})' };
|
|
11205
|
-
}
|
|
11206
|
-
return { data: parsed };
|
|
11207
|
-
} catch (err) {
|
|
11208
|
-
return { error: `invalid JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
11209
|
-
}
|
|
11210
|
-
}
|
|
11211
|
-
try {
|
|
11212
|
-
return { data: parseYamlSubset(node.value) };
|
|
11213
|
-
} catch (err) {
|
|
11214
|
-
return { error: `invalid YAML: ${err instanceof Error ? err.message : String(err)}` };
|
|
11215
|
-
}
|
|
11216
|
-
}
|
|
11217
|
-
function replaceDataFence(contents, data) {
|
|
11218
|
-
const value = JSON.stringify(data, null, 2);
|
|
11219
|
-
const index = contents.findIndex((node) => node.type === "code" && isDataFence(node));
|
|
11220
|
-
if (index < 0) {
|
|
11221
|
-
const fence = { type: "code", lang: "json", meta: "data", value };
|
|
11222
|
-
return [...contents, fence];
|
|
11223
|
-
}
|
|
11224
|
-
const next = [...contents];
|
|
11225
|
-
next[index] = { ...contents[index], value };
|
|
11226
|
-
return next;
|
|
11227
|
-
}
|
|
11228
|
-
function parseScalar(raw) {
|
|
11229
|
-
const trimmed = raw.trim();
|
|
11230
|
-
if (trimmed.length >= 2) {
|
|
11231
|
-
const q = trimmed[0];
|
|
11232
|
-
if ((q === '"' || q === "'") && trimmed.endsWith(q)) {
|
|
11233
|
-
return trimmed.slice(1, -1);
|
|
11234
|
-
}
|
|
11235
|
-
}
|
|
11236
|
-
if (trimmed === "true") return true;
|
|
11237
|
-
if (trimmed === "false") return false;
|
|
11238
|
-
if (trimmed === "null" || trimmed === "~") return null;
|
|
11239
|
-
if (trimmed !== "" && !isNaN(Number(trimmed))) return Number(trimmed);
|
|
11240
|
-
return trimmed;
|
|
11241
|
-
}
|
|
11242
|
-
function splitInlineArray(interior) {
|
|
11243
|
-
const items = [];
|
|
11244
|
-
let current = "";
|
|
11245
|
-
let quote = null;
|
|
11246
|
-
for (const ch of interior) {
|
|
11247
|
-
if (quote) {
|
|
11248
|
-
current += ch;
|
|
11249
|
-
if (ch === quote) quote = null;
|
|
11250
|
-
} else if (ch === '"' || ch === "'") {
|
|
11251
|
-
quote = ch;
|
|
11252
|
-
current += ch;
|
|
11253
|
-
} else if (ch === ",") {
|
|
11254
|
-
items.push(current);
|
|
11255
|
-
current = "";
|
|
11256
|
-
} else {
|
|
11257
|
-
current += ch;
|
|
11258
|
-
}
|
|
11259
|
-
}
|
|
11260
|
-
items.push(current);
|
|
11261
|
-
return items.map((s) => s.trim()).filter((s) => s !== "");
|
|
11262
|
-
}
|
|
11263
|
-
function parseInlineValue(raw) {
|
|
11264
|
-
const trimmed = raw.trim();
|
|
11265
|
-
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
11266
|
-
return splitInlineArray(trimmed.slice(1, -1)).map(parseScalar);
|
|
11267
|
-
}
|
|
11268
|
-
return parseScalar(trimmed);
|
|
11269
|
-
}
|
|
11270
|
-
function parseYamlSubset(src) {
|
|
11271
|
-
const result = {};
|
|
11272
|
-
const lines = src.split("\n");
|
|
11273
|
-
let pendingKey = null;
|
|
11274
|
-
let pendingKind = null;
|
|
11275
|
-
let childIndent = -1;
|
|
11276
|
-
for (let i = 0; i < lines.length; i++) {
|
|
11277
|
-
const line = lines[i];
|
|
11278
|
-
const trimmed = line.trim();
|
|
11279
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
11280
|
-
const indent = line.length - line.trimStart().length;
|
|
11281
|
-
if (indent > 0) {
|
|
11282
|
-
if (!pendingKey) {
|
|
11283
|
-
throw new Error(`line ${i + 1}: unexpected indented line without a preceding "key:" line`);
|
|
11284
|
-
}
|
|
11285
|
-
if (childIndent < 0) childIndent = indent;
|
|
11286
|
-
if (indent > childIndent) {
|
|
11287
|
-
throw new Error(
|
|
11288
|
-
`line ${i + 1}: nesting deeper than one level is not supported \u2014 use a \`\`\`json data fence for nested input`
|
|
11289
|
-
);
|
|
11290
|
-
}
|
|
11291
|
-
if (trimmed.startsWith("- ")) {
|
|
11292
|
-
if (pendingKind === "map") {
|
|
11293
|
-
throw new Error(
|
|
11294
|
-
`line ${i + 1}: cannot mix list items and mapping keys under "${pendingKey}"`
|
|
11295
|
-
);
|
|
11296
|
-
}
|
|
11297
|
-
pendingKind = "list";
|
|
11298
|
-
result[pendingKey].push(parseInlineValue(trimmed.slice(2)));
|
|
11299
|
-
continue;
|
|
11300
|
-
}
|
|
11301
|
-
if (pendingKind === "list") {
|
|
11302
|
-
throw new Error(
|
|
11303
|
-
`line ${i + 1}: cannot mix mapping keys and list items under "${pendingKey}"`
|
|
11304
|
-
);
|
|
11305
|
-
}
|
|
11306
|
-
const subColon = trimmed.indexOf(":");
|
|
11307
|
-
if (subColon < 1) {
|
|
11308
|
-
throw new Error(`line ${i + 1}: expected "key: value"`);
|
|
11309
|
-
}
|
|
11310
|
-
const subValue = trimmed.slice(subColon + 1).trim();
|
|
11311
|
-
if (subValue === "") {
|
|
11312
|
-
throw new Error(
|
|
11313
|
-
`line ${i + 1}: nesting deeper than one level is not supported \u2014 use a \`\`\`json data fence for nested input`
|
|
11314
|
-
);
|
|
11315
|
-
}
|
|
11316
|
-
if (pendingKind !== "map") {
|
|
11317
|
-
pendingKind = "map";
|
|
11318
|
-
result[pendingKey] = {};
|
|
11319
|
-
}
|
|
11320
|
-
result[pendingKey][trimmed.slice(0, subColon).trim()] = parseInlineValue(subValue);
|
|
11321
|
-
continue;
|
|
11322
|
-
}
|
|
11323
|
-
pendingKey = null;
|
|
11324
|
-
pendingKind = null;
|
|
11325
|
-
childIndent = -1;
|
|
11326
|
-
const colonIdx = trimmed.indexOf(":");
|
|
11327
|
-
if (colonIdx < 1) {
|
|
11328
|
-
throw new Error(`line ${i + 1}: expected "key: value"`);
|
|
11329
|
-
}
|
|
11330
|
-
const key = trimmed.slice(0, colonIdx).trim();
|
|
11331
|
-
const rawValue = trimmed.slice(colonIdx + 1).trim();
|
|
11332
|
-
if (rawValue === "") {
|
|
11333
|
-
result[key] = [];
|
|
11334
|
-
pendingKey = key;
|
|
11335
|
-
} else {
|
|
11336
|
-
result[key] = parseInlineValue(rawValue);
|
|
11337
|
-
}
|
|
11338
|
-
}
|
|
11339
|
-
return result;
|
|
11340
|
-
}
|
|
11341
|
-
function findFirstTable(contents) {
|
|
11342
|
-
return contents?.find((n) => n.type === "table");
|
|
11343
|
-
}
|
|
11344
|
-
function extractTableData(table) {
|
|
11345
|
-
const toCells = (row) => row.children.map((cell) => extractPlainText(cell).trim());
|
|
11346
|
-
const [headerRow, ...bodyRows] = table.children;
|
|
11347
|
-
return {
|
|
11348
|
-
headers: headerRow ? toCells(headerRow) : [],
|
|
11349
|
-
rows: bodyRows.map(toCells),
|
|
11350
|
-
...table.align && table.align.some((a) => a != null) ? { align: table.align } : {}
|
|
11351
|
-
};
|
|
11352
|
-
}
|
|
11353
|
-
|
|
11354
12990
|
// src/doc/annotationBlocks.ts
|
|
11355
12991
|
function applyBlockMeta(block, m) {
|
|
11356
12992
|
if (m.build) block.build = m.build;
|
|
@@ -11987,8 +13623,8 @@ function splitIntoSentences(text) {
|
|
|
11987
13623
|
if (fragments.length === 0) return [];
|
|
11988
13624
|
const split = [];
|
|
11989
13625
|
for (const frag of fragments) {
|
|
11990
|
-
const
|
|
11991
|
-
if (
|
|
13626
|
+
const words2 = frag.split(/\s+/);
|
|
13627
|
+
if (words2.length > MAX_PHRASE_WORDS) {
|
|
11992
13628
|
const mid = Math.floor(frag.length / 2);
|
|
11993
13629
|
const clauseRe = /[,;]\s+|\s+—\s+|\s+-\s+/g;
|
|
11994
13630
|
let bestSplit = -1;
|
|
@@ -12290,16 +13926,16 @@ function wordIndexAtChar(script, charOffset) {
|
|
|
12290
13926
|
}
|
|
12291
13927
|
return lo;
|
|
12292
13928
|
}
|
|
12293
|
-
function wordIndexAtTime(
|
|
12294
|
-
if (
|
|
13929
|
+
function wordIndexAtTime(words2, tSec) {
|
|
13930
|
+
if (words2.length === 0) return 0;
|
|
12295
13931
|
let lo = 0;
|
|
12296
|
-
let hi =
|
|
13932
|
+
let hi = words2.length - 1;
|
|
12297
13933
|
while (lo < hi) {
|
|
12298
13934
|
const mid = lo + hi + 1 >> 1;
|
|
12299
|
-
if (
|
|
13935
|
+
if (words2[mid].tSec <= tSec) lo = mid;
|
|
12300
13936
|
else hi = mid - 1;
|
|
12301
13937
|
}
|
|
12302
|
-
return
|
|
13938
|
+
return words2[lo].tSec <= tSec ? lo : 0;
|
|
12303
13939
|
}
|
|
12304
13940
|
|
|
12305
13941
|
// src/doc/builds.ts
|
|
@@ -12333,21 +13969,30 @@ function planContentBuilds(nodes, spec) {
|
|
|
12333
13969
|
}
|
|
12334
13970
|
function blockBuildSteps(block) {
|
|
12335
13971
|
if (!block.build || block.build.mode === "off") return [];
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
)
|
|
13972
|
+
const layers = block.layers ?? [];
|
|
13973
|
+
const tagged = layers.filter((layer) => layer.buildStep !== void 0);
|
|
13974
|
+
if (tagged.length) {
|
|
13975
|
+
const count = Math.max(...tagged.map((layer) => layer.buildStep)) + 1;
|
|
13976
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
13977
|
+
id: `build-${index}`,
|
|
13978
|
+
text: tagged.flatMap(
|
|
13979
|
+
(layer) => layer.buildStep === index && layer.type === "text" ? [layer.content.text] : []
|
|
13980
|
+
).join(" ").trim()
|
|
13981
|
+
}));
|
|
13982
|
+
}
|
|
13983
|
+
return layers.flatMap((layer) => layer.type === "text" ? layer.content.buildSteps ?? [] : []);
|
|
12339
13984
|
}
|
|
12340
13985
|
function buildSourceText(steps) {
|
|
12341
|
-
return JSON.stringify(steps.map((
|
|
13986
|
+
return JSON.stringify(steps.map((step2) => [step2.text, step2.ancestors ?? []]));
|
|
12342
13987
|
}
|
|
12343
13988
|
function resolveBuildTimeline(steps, spec, duration, timing) {
|
|
12344
13989
|
const end = Number.isFinite(duration) ? Math.max(0, duration) : 0;
|
|
12345
13990
|
const requested = Number.isFinite(spec.duration) ? Math.max(0, spec.duration) : 0.4;
|
|
12346
|
-
let
|
|
13991
|
+
let entrance2 = Math.min(requested, end / Math.max(4, steps.length * 2));
|
|
12347
13992
|
const intro = Math.min(0.5, end * 0.08);
|
|
12348
13993
|
const hold = Math.min(1.5, end * 0.2);
|
|
12349
|
-
const latest = Math.max(0, end - hold -
|
|
12350
|
-
const weights = steps.map((
|
|
13994
|
+
const latest = Math.max(0, end - hold - entrance2);
|
|
13995
|
+
const weights = steps.map((step2) => Math.max(1, step2.text.trim().split(/\s+/).length));
|
|
12351
13996
|
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
|
12352
13997
|
let consumed = 0;
|
|
12353
13998
|
let previous = 0;
|
|
@@ -12363,21 +14008,21 @@ function resolveBuildTimeline(steps, spec, duration, timing) {
|
|
|
12363
14008
|
return start;
|
|
12364
14009
|
});
|
|
12365
14010
|
if (matched && starts.length)
|
|
12366
|
-
|
|
12367
|
-
|
|
14011
|
+
entrance2 = Math.min(
|
|
14012
|
+
entrance2,
|
|
12368
14013
|
Math.max(0, end - starts[starts.length - 1] - Math.min(0.04, end / 8))
|
|
12369
14014
|
);
|
|
12370
|
-
return { starts, entrance, compressed: steps.length > 0 && end < steps.length * 0.8 };
|
|
14015
|
+
return { starts, entrance: entrance2, compressed: steps.length > 0 && end < steps.length * 0.8 };
|
|
12371
14016
|
}
|
|
12372
|
-
function buildCursorAtTime(
|
|
14017
|
+
function buildCursorAtTime(timeline2, time) {
|
|
12373
14018
|
let revealed = 0;
|
|
12374
|
-
for (const start of
|
|
14019
|
+
for (const start of timeline2.starts) {
|
|
12375
14020
|
if (time < start) break;
|
|
12376
14021
|
revealed++;
|
|
12377
14022
|
}
|
|
12378
14023
|
return {
|
|
12379
14024
|
revealed,
|
|
12380
|
-
progress: revealed === 0 ? 0 :
|
|
14025
|
+
progress: revealed === 0 ? 0 : timeline2.entrance === 0 ? 1 : Math.min(1, Math.max(0, (time - timeline2.starts[revealed - 1]) / timeline2.entrance))
|
|
12381
14026
|
};
|
|
12382
14027
|
}
|
|
12383
14028
|
function buildTargetState(index, steps, cursor, spec, motion = true) {
|
|
@@ -12410,11 +14055,11 @@ function resolveDocBuildTiming(doc, blocks) {
|
|
|
12410
14055
|
if (recorded?.length) {
|
|
12411
14056
|
const automatic = resolveBuildTimeline(steps, block.build, block.duration);
|
|
12412
14057
|
let last = 0;
|
|
12413
|
-
const starts2 = steps.map((_,
|
|
12414
|
-
const event = recorded.find((event2) => event2.step ===
|
|
12415
|
-
const following = recorded.filter((event2) => event2.step >
|
|
14058
|
+
const starts2 = steps.map((_, step2) => {
|
|
14059
|
+
const event = recorded.find((event2) => event2.step === step2);
|
|
14060
|
+
const following = recorded.filter((event2) => event2.step > step2).sort((a, b) => a.step - b.step)[0];
|
|
12416
14061
|
const estimate = Math.min(
|
|
12417
|
-
automatic.starts[
|
|
14062
|
+
automatic.starts[step2],
|
|
12418
14063
|
following ? Math.max(0, following.time - block.startTime) : block.duration
|
|
12419
14064
|
);
|
|
12420
14065
|
last = Math.max(last, event ? event.time - block.startTime : estimate);
|
|
@@ -12432,8 +14077,8 @@ function resolveDocBuildTiming(doc, blocks) {
|
|
|
12432
14077
|
if (!source) return block;
|
|
12433
14078
|
let cursor = 0;
|
|
12434
14079
|
const starts = [];
|
|
12435
|
-
for (const
|
|
12436
|
-
const text =
|
|
14080
|
+
for (const step2 of steps) {
|
|
14081
|
+
const text = step2.text.trim();
|
|
12437
14082
|
const offset = text ? source.text.indexOf(text, cursor) : -1;
|
|
12438
14083
|
if (offset < 0) return block;
|
|
12439
14084
|
const word = narration.bookmarks.find(
|
|
@@ -12450,13 +14095,14 @@ function resolveDocBuildTiming(doc, blocks) {
|
|
|
12450
14095
|
// src/doc/templates/contentBlock.ts
|
|
12451
14096
|
function bodyNodes(context) {
|
|
12452
14097
|
if (context.block?.summaryMode === "headings-and-features") return [];
|
|
14098
|
+
const summary = !!context.block?.summaryMode;
|
|
12453
14099
|
return (context.block?.contents ?? []).filter(
|
|
12454
14100
|
// Mermaid source, hosted-video links and picture-only paragraphs are
|
|
12455
14101
|
// already materialized as visual layers (a diagram, a player, the picture
|
|
12456
14102
|
// itself). Repeating them as prose makes the loss-averse default noisy
|
|
12457
14103
|
// without preserving any additional meaning — a pasted picture's alt text
|
|
12458
14104
|
// is just its generated file name.
|
|
12459
|
-
(node) => !isMaterializedMediaNode(node) && !isPictureParagraph(node)
|
|
14105
|
+
(node) => !isMaterializedMediaNode(node) && !isPictureParagraph(node) && !(summary && (node.type === "table" || node.type === "code"))
|
|
12460
14106
|
);
|
|
12461
14107
|
}
|
|
12462
14108
|
function listParagraphs(list, level = 0) {
|
|
@@ -12599,6 +14245,12 @@ export {
|
|
|
12599
14245
|
countUpSpecFor,
|
|
12600
14246
|
formatCountValue,
|
|
12601
14247
|
formatCountUp,
|
|
14248
|
+
splitConceptItem,
|
|
14249
|
+
conceptBuildStepTexts,
|
|
14250
|
+
cardsBlock,
|
|
14251
|
+
processBlock,
|
|
14252
|
+
cycleBlock,
|
|
14253
|
+
layersBlock,
|
|
12602
14254
|
statHighlight,
|
|
12603
14255
|
quoteBlock,
|
|
12604
14256
|
factCard,
|
|
@@ -12652,21 +14304,27 @@ export {
|
|
|
12652
14304
|
coerceTemplateParams,
|
|
12653
14305
|
lintTemplateParams,
|
|
12654
14306
|
fallbackBlockLayers,
|
|
14307
|
+
isDataFence,
|
|
14308
|
+
parseDataFence,
|
|
14309
|
+
replaceDataFence,
|
|
14310
|
+
parseYamlSubset,
|
|
14311
|
+
findFirstTable,
|
|
14312
|
+
extractTableData,
|
|
12655
14313
|
materializeBlockLayers,
|
|
12656
14314
|
materializeBlockLayersWithRuntime,
|
|
12657
14315
|
TEMPLATE_METADATA,
|
|
12658
14316
|
TEMPLATE_AUTHORING_METADATA,
|
|
14317
|
+
TEMPLATE_GUIDES,
|
|
14318
|
+
guideText,
|
|
14319
|
+
templateCatalog,
|
|
14320
|
+
PRESENTATION_LAYOUT_GUIDE,
|
|
14321
|
+
AUTHORING_TRICKS,
|
|
14322
|
+
authoringTricks,
|
|
12659
14323
|
coverBlock,
|
|
12660
14324
|
expandCoverBlock,
|
|
12661
14325
|
expandDocBlocks,
|
|
12662
14326
|
getAvailableTemplates,
|
|
12663
14327
|
hasTemplate,
|
|
12664
|
-
isDataFence,
|
|
12665
|
-
parseDataFence,
|
|
12666
|
-
replaceDataFence,
|
|
12667
|
-
parseYamlSubset,
|
|
12668
|
-
findFirstTable,
|
|
12669
|
-
extractTableData,
|
|
12670
14328
|
detectPromotableBodyAnnotation,
|
|
12671
14329
|
getPinnedBlockMeta,
|
|
12672
14330
|
markdownToDoc,
|