@bendyline/squisq 1.4.1 → 1.5.0
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-BeSeH2iX.d.ts → Doc-BsMkmzH8.d.ts} +838 -268
- package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-DvoeHMP9.d.ts} +11 -4
- package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
- package/dist/chunk-2OIBZYKP.js.map +1 -0
- package/dist/chunk-2UQOHYE2.js +45 -0
- package/dist/chunk-2UQOHYE2.js.map +1 -0
- package/dist/{chunk-B4EEON3N.js → chunk-4AKUQPEE.js} +536 -103
- package/dist/chunk-4AKUQPEE.js.map +1 -0
- package/dist/chunk-4ZFRZBU2.js +6582 -0
- package/dist/chunk-4ZFRZBU2.js.map +1 -0
- package/dist/{chunk-57CVQCFX.js → chunk-7MQGNR76.js} +79 -3
- package/dist/chunk-7MQGNR76.js.map +1 -0
- package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
- package/dist/chunk-DLXZMS5K.js.map +1 -0
- package/dist/{chunk-MYIH7FWD.js → chunk-FBKP5CAO.js} +1 -1
- package/dist/chunk-FBKP5CAO.js.map +1 -0
- package/dist/chunk-KZNXZ5SM.js +333 -0
- package/dist/chunk-KZNXZ5SM.js.map +1 -0
- package/dist/{chunk-WMOI7FN3.js → chunk-LRCV7N7F.js} +12 -2
- package/dist/{chunk-WMOI7FN3.js.map → chunk-LRCV7N7F.js.map} +1 -1
- package/dist/{chunk-3K5OG6KI.js → chunk-OH43RPTP.js} +403 -187
- package/dist/chunk-OH43RPTP.js.map +1 -0
- package/dist/{chunk-H3AXU4MJ.js → chunk-OKJOQXK4.js} +149 -2
- package/dist/chunk-OKJOQXK4.js.map +1 -0
- package/dist/chunk-PGGGKYWG.js +1 -0
- package/dist/chunk-PGGGKYWG.js.map +1 -0
- package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
- package/dist/chunk-Q3ROPT5H.js.map +1 -0
- package/dist/chunk-QE7TWGVT.js +178 -0
- package/dist/chunk-QE7TWGVT.js.map +1 -0
- package/dist/chunk-RUDYOTA6.js +345 -0
- package/dist/chunk-RUDYOTA6.js.map +1 -0
- package/dist/chunk-TVSGLUID.js +596 -0
- package/dist/chunk-TVSGLUID.js.map +1 -0
- package/dist/chunk-ZLQKNOZQ.js +1 -0
- package/dist/chunk-ZLQKNOZQ.js.map +1 -0
- package/dist/doc/index.d.ts +4 -4
- package/dist/doc/index.js +124 -6
- package/dist/generate/index.d.ts +9 -2
- package/dist/generate/index.js +1 -1
- package/dist/icons/index.d.ts +3 -26
- package/dist/icons/index.js +13 -0
- package/dist/icons/inlineIconMarker.d.ts +1 -0
- package/dist/icons/inlineIconMarker.js +15 -0
- package/dist/icons/inlineIconMarker.js.map +1 -0
- package/dist/imageEdit/index.d.ts +11 -5
- package/dist/imageEdit/index.js +4 -1
- package/dist/index.d.ts +11 -8
- package/dist/index.js +243 -24
- package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
- package/dist/markdown/index.d.ts +212 -3
- package/dist/markdown/index.js +40 -7
- package/dist/recommend/index.d.ts +1 -1
- package/dist/recommend/index.js +3 -2
- package/dist/schemas/index.d.ts +28 -8
- package/dist/schemas/index.js +35 -5
- package/dist/spatial/index.d.ts +9 -4
- package/dist/spatial/index.js +1 -1
- package/dist/story/index.d.ts +906 -40
- package/dist/story/index.js +124 -6
- package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-DQQKuRMx.d.ts} +1 -1
- package/dist/transform/index.d.ts +48 -10
- package/dist/transform/index.js +17 -481
- package/dist/transform/index.js.map +1 -1
- package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
- package/package.json +6 -1
- package/src/__tests__/animationUtils.test.ts +40 -0
- package/src/__tests__/annotationCoercion.test.ts +243 -0
- package/src/__tests__/applyRenderStyle.test.ts +146 -0
- package/src/__tests__/atmosphereLayers.test.ts +108 -0
- package/src/__tests__/attrTokens.test.ts +162 -0
- package/src/__tests__/customTemplateSample.test.ts +83 -0
- package/src/__tests__/customTemplatesFrontmatter.test.ts +205 -0
- package/src/__tests__/customThemesFrontmatter.test.ts +118 -0
- package/src/__tests__/diagramBlock.test.ts +122 -0
- package/src/__tests__/diagramLayout.test.ts +134 -0
- package/src/__tests__/drawing.test.ts +109 -0
- package/src/__tests__/drawingBlock.test.ts +124 -0
- package/src/__tests__/drawingLayout.test.ts +171 -0
- package/src/__tests__/geohash.test.ts +6 -5
- package/src/__tests__/getLayers.test.ts +6 -2
- package/src/__tests__/getLayersFallback.test.ts +103 -0
- package/src/__tests__/imageEditExportSvg.test.ts +70 -0
- package/src/__tests__/imageEditVersions.test.ts +72 -0
- package/src/__tests__/inlineIconMarker.test.ts +36 -0
- package/src/__tests__/layoutBlock.test.ts +83 -0
- package/src/__tests__/layoutLayout.test.ts +169 -0
- package/src/__tests__/markdown.test.ts +23 -0
- package/src/__tests__/markdownSanitize.test.ts +64 -0
- package/src/__tests__/markdownToDoc.test.ts +223 -2
- package/src/__tests__/mediaAnnotations.test.ts +64 -0
- package/src/__tests__/mediaSchedule.test.ts +118 -0
- package/src/__tests__/pandocAttrParse.test.ts +187 -0
- package/src/__tests__/resolveDocTheme.test.ts +55 -0
- package/src/__tests__/rootExports.test.ts +43 -0
- package/src/__tests__/shapeGeometry.test.ts +89 -0
- package/src/__tests__/shapeVocabulary.test.ts +133 -0
- package/src/__tests__/structuredData.test.ts +179 -0
- package/src/__tests__/templateAnnotationParse.test.ts +108 -0
- package/src/__tests__/templateMetadata.test.ts +21 -0
- package/src/__tests__/templates.test.ts +145 -4
- package/src/__tests__/themeCompile.test.ts +42 -0
- package/src/__tests__/themeValidator.test.ts +6 -0
- package/src/__tests__/transformV2.test.ts +137 -0
- package/src/__tests__/transitionNormalize.test.ts +61 -0
- package/src/__tests__/validateDoc.test.ts +100 -0
- package/src/doc/customTemplatesFrontmatter.ts +308 -0
- package/src/doc/customThemesFrontmatter.ts +90 -0
- package/src/doc/docToMarkdown.ts +73 -4
- package/src/doc/getLayers.ts +40 -17
- package/src/doc/index.ts +35 -1
- package/src/doc/markdownToDoc.ts +333 -12
- package/src/doc/mediaAnnotations.ts +118 -0
- package/src/doc/resolveDocTheme.ts +55 -0
- package/src/doc/structuredData.ts +230 -0
- package/src/doc/templateInputs.ts +240 -0
- package/src/doc/templates/__tests__/customTemplate.test.ts +151 -0
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
- package/src/doc/templates/accentImage.ts +30 -6
- package/src/doc/templates/captionUtils.ts +23 -0
- package/src/doc/templates/comparisonBar.ts +26 -18
- package/src/doc/templates/coverBlock.ts +7 -4
- package/src/doc/templates/customTemplate.ts +45 -0
- package/src/doc/templates/dataTable.ts +37 -19
- package/src/doc/templates/dateEvent.ts +41 -20
- package/src/doc/templates/definitionCard.ts +26 -18
- package/src/doc/templates/diagramBlock.ts +204 -0
- package/src/doc/templates/diagramLayout.ts +159 -0
- package/src/doc/templates/drawingBlock.ts +325 -0
- package/src/doc/templates/drawingLayout.ts +539 -0
- package/src/doc/templates/factCard.ts +51 -25
- package/src/doc/templates/fallbackBlock.ts +123 -0
- package/src/doc/templates/featureBlock.ts +20 -13
- package/src/doc/templates/fullBleedQuote.ts +18 -8
- package/src/doc/templates/imageWithCaption.ts +22 -9
- package/src/doc/templates/index.ts +215 -46
- package/src/doc/templates/layoutBlock.ts +43 -0
- package/src/doc/templates/layoutLayout.ts +243 -0
- package/src/doc/templates/listBlock.ts +56 -30
- package/src/doc/templates/mapBlock.ts +15 -12
- package/src/doc/templates/metadata.ts +120 -0
- package/src/doc/templates/persistentLayers.ts +129 -11
- package/src/doc/templates/photoGrid.ts +23 -12
- package/src/doc/templates/pullQuote.ts +30 -12
- package/src/doc/templates/quoteBlock.ts +54 -29
- package/src/doc/templates/sectionHeader.ts +25 -11
- package/src/doc/templates/statHighlight.ts +39 -22
- package/src/doc/templates/titleBlock.ts +59 -21
- package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +221 -0
- package/src/doc/templates/tokens/resolveTokens.ts +0 -0
- package/src/doc/templates/twoColumn.ts +48 -30
- package/src/doc/templates/videoPullQuote.ts +22 -12
- package/src/doc/templates/videoWithCaption.ts +13 -8
- package/src/doc/utils/animationUtils.ts +267 -3
- package/src/doc/utils/applyRenderStyle.ts +113 -0
- package/src/doc/utils/imageTreatment.ts +70 -0
- package/src/doc/utils/shapeGeometry.ts +464 -0
- package/src/doc/utils/themeUtils.ts +75 -1
- package/src/doc/validate.ts +476 -0
- package/src/generate/slideshowGenerator.ts +7 -0
- package/src/icons/index.ts +8 -0
- package/src/icons/inlineIconMarker.ts +73 -0
- package/src/imageEdit/export.ts +85 -2
- package/src/imageEdit/index.ts +1 -0
- package/src/imageEdit/persistence.ts +51 -0
- package/src/index.ts +2 -0
- package/src/markdown/annotationCoercion.ts +295 -0
- package/src/markdown/attrTokens.ts +222 -0
- package/src/markdown/convert.ts +229 -26
- package/src/markdown/index.ts +36 -0
- package/src/markdown/sanitize.ts +298 -0
- package/src/markdown/stringify.ts +84 -3
- package/src/markdown/types.ts +66 -0
- package/src/markdown/utils.ts +49 -1
- package/src/recommend/templates.ts +24 -0
- package/src/schemas/BlockTemplates.ts +133 -4
- package/src/schemas/CustomTemplates.ts +74 -0
- package/src/schemas/Doc.ts +353 -29
- package/src/schemas/ImageEditDoc.ts +11 -4
- package/src/schemas/Media.ts +169 -0
- package/src/schemas/Theme.ts +23 -1
- package/src/schemas/Transitions.ts +224 -0
- package/src/schemas/colorUtils.ts +33 -0
- package/src/schemas/fontStacks.ts +5 -2
- package/src/schemas/index.ts +3 -0
- package/src/schemas/themeCompile.ts +23 -4
- package/src/schemas/themeValidator.ts +49 -11
- package/src/schemas/themes/cinematic.json +69 -12
- package/src/schemas/themes/documentary.json +58 -11
- package/src/schemas/themes/gezellig.json +60 -11
- package/src/schemas/themes/magazine.json +54 -9
- package/src/schemas/themes/morning-light.json +52 -9
- package/src/schemas/themes/standard-dark.json +48 -13
- package/src/schemas/themes/standard.json +44 -12
- package/src/schemas/themes/tech-dark.json +65 -12
- package/src/spatial/Geohash.ts +52 -21
- package/src/transform/applyTransform.ts +43 -4
- package/src/transform/index.ts +1 -0
- package/src/transform/registry.ts +44 -13
- package/src/transform/styles/dataDriven.ts +2 -1
- package/src/transform/styles/documentary.ts +2 -1
- package/src/transform/styles/magazine.ts +3 -1
- package/src/transform/styles/minimal.ts +2 -1
- package/src/transform/styles/narrative.ts +3 -1
- package/src/transform/templateSelector.ts +87 -11
- package/src/transform/types.ts +35 -2
- package/dist/chunk-3K5OG6KI.js.map +0 -1
- package/dist/chunk-57CVQCFX.js.map +0 -1
- package/dist/chunk-5WFRKURX.js.map +0 -1
- package/dist/chunk-7UDSRZKG.js.map +0 -1
- package/dist/chunk-B4EEON3N.js.map +0 -1
- package/dist/chunk-H3AXU4MJ.js.map +0 -1
- package/dist/chunk-KSWJR66U.js +0 -3706
- package/dist/chunk-KSWJR66U.js.map +0 -1
- package/dist/chunk-MYIH7FWD.js.map +0 -1
- package/dist/chunk-ZTX4PFFT.js.map +0 -1
|
@@ -10,18 +10,28 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Layer } from '../../schemas/Doc.js';
|
|
12
12
|
import type { PullQuoteInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
getThemeFont,
|
|
15
|
+
themedEntrance,
|
|
16
|
+
themedFontSize,
|
|
17
|
+
themedImageTreatment,
|
|
18
|
+
} from '../utils/themeUtils.js';
|
|
19
|
+
import { estimateTextHeight } from './captionUtils.js';
|
|
15
20
|
|
|
16
21
|
export function pullQuote(input: PullQuoteInput, context: TemplateContext): Layer[] {
|
|
17
22
|
const { text, attribution, backgroundImage, ambientMotion } = input;
|
|
23
|
+
const { viewport } = context;
|
|
24
|
+
|
|
25
|
+
const treatment = themedImageTreatment(context, input.imageTreatment);
|
|
18
26
|
|
|
19
27
|
// Guard: backgroundImage is required
|
|
20
28
|
if (!backgroundImage?.src) return [];
|
|
21
29
|
|
|
22
|
-
const quoteFontSize =
|
|
23
|
-
const attrFontSize =
|
|
24
|
-
const decoFontSize =
|
|
30
|
+
const quoteFontSize = themedFontSize(52, context, true);
|
|
31
|
+
const attrFontSize = themedFontSize(26, context, false);
|
|
32
|
+
const decoFontSize = themedFontSize(200, context, true);
|
|
33
|
+
const quoteLineHeight = 1.4;
|
|
34
|
+
const quoteYPct = attribution ? 45 : 50;
|
|
25
35
|
|
|
26
36
|
const layers: Layer[] = [
|
|
27
37
|
// Full-bleed background image
|
|
@@ -34,6 +44,7 @@ export function pullQuote(input: PullQuoteInput, context: TemplateContext): Laye
|
|
|
34
44
|
fit: 'cover',
|
|
35
45
|
credit: backgroundImage.credit,
|
|
36
46
|
license: backgroundImage.license,
|
|
47
|
+
...(treatment ? { treatment } : {}),
|
|
37
48
|
},
|
|
38
49
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
39
50
|
animation: ambientMotion ? { type: ambientMotion, duration: 15 } : undefined,
|
|
@@ -81,22 +92,29 @@ export function pullQuote(input: PullQuoteInput, context: TemplateContext): Laye
|
|
|
81
92
|
fontFamily: getThemeFont(context, 'title'),
|
|
82
93
|
color: '#ffffff',
|
|
83
94
|
textAlign: 'center',
|
|
84
|
-
lineHeight:
|
|
95
|
+
lineHeight: quoteLineHeight,
|
|
85
96
|
shadow: true,
|
|
86
97
|
},
|
|
87
98
|
},
|
|
88
99
|
position: {
|
|
89
100
|
x: '50%',
|
|
90
|
-
y:
|
|
101
|
+
y: `${quoteYPct}%`,
|
|
91
102
|
anchor: 'center',
|
|
92
|
-
width: '
|
|
103
|
+
width: '72%',
|
|
93
104
|
},
|
|
94
|
-
animation: { type: 'fadeIn', duration: 2 },
|
|
105
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 2 }),
|
|
95
106
|
},
|
|
96
107
|
];
|
|
97
108
|
|
|
98
|
-
// Attribution
|
|
109
|
+
// Attribution \u2014 hangs just below the quote's estimated bottom edge so
|
|
110
|
+
// the two read as one lockup instead of stranding near the bottom.
|
|
99
111
|
if (attribution) {
|
|
112
|
+
const quoteWidthPx = 0.72 * viewport.width;
|
|
113
|
+
const quoteHeightPx = estimateTextHeight(text, quoteFontSize, quoteWidthPx, quoteLineHeight);
|
|
114
|
+
const attrYPct = Math.min(
|
|
115
|
+
82,
|
|
116
|
+
quoteYPct + ((quoteHeightPx / 2 + quoteFontSize * 1.7) / viewport.height) * 100,
|
|
117
|
+
);
|
|
100
118
|
layers.push({
|
|
101
119
|
type: 'text',
|
|
102
120
|
id: 'attribution',
|
|
@@ -105,14 +123,14 @@ export function pullQuote(input: PullQuoteInput, context: TemplateContext): Laye
|
|
|
105
123
|
style: {
|
|
106
124
|
fontSize: attrFontSize,
|
|
107
125
|
fontFamily: getThemeFont(context, 'body'),
|
|
108
|
-
color: 'rgba(255, 255, 255, 0.
|
|
126
|
+
color: 'rgba(255, 255, 255, 0.85)',
|
|
109
127
|
textAlign: 'center',
|
|
110
128
|
shadow: true,
|
|
111
129
|
},
|
|
112
130
|
},
|
|
113
131
|
position: {
|
|
114
132
|
x: '50%',
|
|
115
|
-
y:
|
|
133
|
+
y: `${attrYPct}%`,
|
|
116
134
|
anchor: 'center',
|
|
117
135
|
},
|
|
118
136
|
animation: { type: 'fadeIn', duration: 1, delay: 1.5 },
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quote Block Template
|
|
3
3
|
*
|
|
4
|
-
* Large centered quote with optional attribution
|
|
5
|
-
*
|
|
4
|
+
* Large centered quote with optional attribution, composed as one
|
|
5
|
+
* vertically-centered lockup: the attribution hangs a fixed distance
|
|
6
|
+
* below the quote's estimated bottom edge instead of being pinned to a
|
|
7
|
+
* far-away layout slot.
|
|
6
8
|
* Adapts font sizes and positioning for different viewports.
|
|
7
9
|
*
|
|
8
10
|
* Supports optional accent images that appear as tasteful side/bottom strips.
|
|
@@ -12,59 +14,72 @@
|
|
|
12
14
|
|
|
13
15
|
import type { Layer } from '../../schemas/Doc.js';
|
|
14
16
|
import type { QuoteBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
+
import {
|
|
18
|
+
getThemeFont,
|
|
19
|
+
shouldUseShadow,
|
|
20
|
+
themedEntrance,
|
|
21
|
+
themedFontSize,
|
|
22
|
+
themedSurfaceGradient,
|
|
23
|
+
themedImageTreatment,
|
|
24
|
+
} from '../utils/themeUtils.js';
|
|
25
|
+
import { withAlpha } from '../../schemas/colorUtils.js';
|
|
17
26
|
import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
|
|
18
|
-
import { createBackgroundLayer } from './captionUtils.js';
|
|
27
|
+
import { createBackgroundLayer, estimateTextHeight } from './captionUtils.js';
|
|
19
28
|
|
|
20
29
|
export function quoteBlock(input: QuoteBlockInput, context: TemplateContext): Layer[] {
|
|
21
30
|
const { quote, attribution, accentImage } = input;
|
|
22
|
-
const { theme,
|
|
31
|
+
const { theme, viewport } = context;
|
|
23
32
|
|
|
24
33
|
// Get layout adjustments if accent image is present
|
|
25
34
|
const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
|
|
26
35
|
|
|
27
36
|
// Scale font sizes for viewport
|
|
28
|
-
const quoteFontSize =
|
|
29
|
-
const attrFontSize =
|
|
37
|
+
const quoteFontSize = themedFontSize(48, context, true);
|
|
38
|
+
const attrFontSize = themedFontSize(26, context, false);
|
|
39
|
+
const quoteLineHeight = Math.max(theme.typography.titleLineHeight ?? 1.4, 1.25);
|
|
30
40
|
|
|
31
41
|
// Decorative quotation mark font size
|
|
32
|
-
const decorativeQuoteFontSize =
|
|
42
|
+
const decorativeQuoteFontSize = themedFontSize(280, context, true);
|
|
33
43
|
|
|
34
|
-
const layers: Layer[] = [
|
|
35
|
-
createBackgroundLayer(
|
|
36
|
-
'bg',
|
|
37
|
-
`linear-gradient(160deg, ${theme.colors.backgroundLight} 0%, #1e2636 100%)`,
|
|
38
|
-
),
|
|
39
|
-
];
|
|
44
|
+
const layers: Layer[] = [createBackgroundLayer('bg', themedSurfaceGradient(context, 160))];
|
|
40
45
|
|
|
41
46
|
// Add accent image layers (behind text, after background)
|
|
42
47
|
if (accentImage) {
|
|
43
|
-
layers.push(
|
|
48
|
+
layers.push(
|
|
49
|
+
...createAccentLayers(
|
|
50
|
+
accentImage,
|
|
51
|
+
input.id,
|
|
52
|
+
themedImageTreatment(context, input.imageTreatment),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
// Decorative opening quotation mark — oversized, low-opacity behind
|
|
57
|
+
// Decorative opening quotation mark — oversized, low-opacity behind the
|
|
58
|
+
// quote. Derived from the theme text color so it stays a subtle ornament
|
|
59
|
+
// on light and dark surfaces alike (a hard-coded white glyph was
|
|
60
|
+
// invisible on both).
|
|
47
61
|
layers.push({
|
|
48
62
|
type: 'text',
|
|
49
63
|
id: 'deco-quote',
|
|
50
64
|
content: {
|
|
51
|
-
text: '
|
|
65
|
+
text: '“',
|
|
52
66
|
style: {
|
|
53
67
|
fontSize: decorativeQuoteFontSize,
|
|
54
68
|
fontFamily: getThemeFont(context, 'title'),
|
|
55
|
-
color:
|
|
69
|
+
color: withAlpha(theme.colors.text, 0.09),
|
|
56
70
|
textAlign: 'center',
|
|
57
71
|
},
|
|
58
72
|
},
|
|
59
73
|
position: {
|
|
60
74
|
x: accentLayout.textCenterX,
|
|
61
|
-
y: adjustY('
|
|
75
|
+
y: adjustY('26%', accentLayout),
|
|
62
76
|
anchor: 'center',
|
|
63
77
|
},
|
|
64
78
|
});
|
|
65
79
|
|
|
66
|
-
// Quote text
|
|
67
|
-
|
|
80
|
+
// Quote text — optically centered, nudged up slightly when an
|
|
81
|
+
// attribution hangs below it.
|
|
82
|
+
const quoteYPct = attribution ? 45 : 50;
|
|
68
83
|
layers.push({
|
|
69
84
|
type: 'text',
|
|
70
85
|
id: 'quote',
|
|
@@ -75,21 +90,31 @@ export function quoteBlock(input: QuoteBlockInput, context: TemplateContext): La
|
|
|
75
90
|
fontFamily: getThemeFont(context, 'title'),
|
|
76
91
|
color: theme.colors.text,
|
|
77
92
|
textAlign: 'center',
|
|
78
|
-
lineHeight:
|
|
79
|
-
shadow:
|
|
93
|
+
lineHeight: quoteLineHeight,
|
|
94
|
+
shadow: shouldUseShadow(context),
|
|
80
95
|
},
|
|
81
96
|
},
|
|
82
97
|
position: {
|
|
83
98
|
x: accentLayout.textCenterX,
|
|
84
|
-
y: adjustY(
|
|
99
|
+
y: adjustY(`${quoteYPct}%`, accentLayout),
|
|
85
100
|
anchor: 'center',
|
|
86
101
|
width: accentLayout.textWidth,
|
|
87
102
|
},
|
|
88
|
-
animation: { type: 'fadeIn', duration: 2 },
|
|
103
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 2 }),
|
|
89
104
|
});
|
|
90
105
|
|
|
91
|
-
//
|
|
106
|
+
// Attribution hangs just below the quote's estimated bottom edge, so
|
|
107
|
+
// the two read as one lockup instead of the attribution stranding near
|
|
108
|
+
// the bottom of the block.
|
|
92
109
|
if (attribution) {
|
|
110
|
+
const quoteWidthPx = (parseFloat(accentLayout.textWidth) / 100) * viewport.width;
|
|
111
|
+
const quoteHeightPx = estimateTextHeight(quote, quoteFontSize, quoteWidthPx, quoteLineHeight);
|
|
112
|
+
const gapPx = quoteFontSize * 1.7;
|
|
113
|
+
const attrYPct = Math.min(
|
|
114
|
+
82,
|
|
115
|
+
quoteYPct + ((quoteHeightPx / 2 + gapPx) / viewport.height) * 100,
|
|
116
|
+
);
|
|
117
|
+
|
|
93
118
|
layers.push({
|
|
94
119
|
type: 'text',
|
|
95
120
|
id: 'attribution',
|
|
@@ -100,12 +125,12 @@ export function quoteBlock(input: QuoteBlockInput, context: TemplateContext): La
|
|
|
100
125
|
fontFamily: getThemeFont(context, 'body'),
|
|
101
126
|
color: theme.colors.textMuted,
|
|
102
127
|
textAlign: 'center',
|
|
103
|
-
shadow:
|
|
128
|
+
shadow: shouldUseShadow(context),
|
|
104
129
|
},
|
|
105
130
|
},
|
|
106
131
|
position: {
|
|
107
132
|
x: accentLayout.textCenterX,
|
|
108
|
-
y: adjustY(
|
|
133
|
+
y: adjustY(`${attrYPct}%`, accentLayout),
|
|
109
134
|
anchor: 'center',
|
|
110
135
|
},
|
|
111
136
|
animation: { type: 'fadeIn', duration: 1, delay: 1.5 },
|
|
@@ -11,16 +11,27 @@
|
|
|
11
11
|
|
|
12
12
|
import type { Layer, AnimationType } from '../../schemas/Doc.js';
|
|
13
13
|
import type { SectionHeaderInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
resolveColorScheme,
|
|
16
|
+
getThemeFont,
|
|
17
|
+
shouldUseShadow,
|
|
18
|
+
themedEntrance,
|
|
19
|
+
themedFontSize,
|
|
20
|
+
themedScrim,
|
|
21
|
+
themedImageTreatment,
|
|
22
|
+
} from '../utils/themeUtils.js';
|
|
16
23
|
|
|
17
24
|
export function sectionHeader(input: SectionHeaderInput, context: TemplateContext): Layer[] {
|
|
18
25
|
const { title = '', colorScheme = 'blue', imageSrc, imageAlt, ambientMotion } = input;
|
|
19
|
-
const { layout } = context;
|
|
26
|
+
const { theme, layout } = context;
|
|
27
|
+
|
|
28
|
+
const treatment = themedImageTreatment(context, input.imageTreatment);
|
|
20
29
|
const colors = resolveColorScheme(context, colorScheme);
|
|
21
30
|
|
|
22
|
-
// Scale font sizes for viewport
|
|
23
|
-
|
|
31
|
+
// Scale font sizes for viewport. Section dividers are the loudest
|
|
32
|
+
// interstitial in a doc, so the title sits a step above ordinary
|
|
33
|
+
// content-block headings.
|
|
34
|
+
const titleFontSize = themedFontSize(84, context, true);
|
|
24
35
|
|
|
25
36
|
const layers: Layer[] = [];
|
|
26
37
|
|
|
@@ -34,6 +45,7 @@ export function sectionHeader(input: SectionHeaderInput, context: TemplateContex
|
|
|
34
45
|
src: imageSrc,
|
|
35
46
|
alt: imageAlt || title,
|
|
36
47
|
fit: 'cover',
|
|
48
|
+
...(treatment ? { treatment } : {}),
|
|
37
49
|
},
|
|
38
50
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
39
51
|
animation: ambientMotion
|
|
@@ -41,13 +53,14 @@ export function sectionHeader(input: SectionHeaderInput, context: TemplateContex
|
|
|
41
53
|
: { type: 'slowZoom', duration: 8, direction: 'in' },
|
|
42
54
|
});
|
|
43
55
|
|
|
44
|
-
//
|
|
56
|
+
// Theme-tinted overlay for text readability — light themes get a
|
|
57
|
+
// light scrim with dark theme text, dark themes the familiar dark one.
|
|
45
58
|
layers.push({
|
|
46
59
|
type: 'shape',
|
|
47
60
|
id: 'overlay',
|
|
48
61
|
content: {
|
|
49
62
|
shape: 'rect',
|
|
50
|
-
fill:
|
|
63
|
+
fill: themedScrim(context),
|
|
51
64
|
},
|
|
52
65
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
53
66
|
});
|
|
@@ -78,7 +91,8 @@ export function sectionHeader(input: SectionHeaderInput, context: TemplateContex
|
|
|
78
91
|
});
|
|
79
92
|
}
|
|
80
93
|
|
|
81
|
-
// Section title
|
|
94
|
+
// Section title — theme text over the theme-tinted scrim (readable by
|
|
95
|
+
// construction), scheme text on the solid-color fallback
|
|
82
96
|
layers.push({
|
|
83
97
|
type: 'text',
|
|
84
98
|
id: 'title',
|
|
@@ -88,9 +102,9 @@ export function sectionHeader(input: SectionHeaderInput, context: TemplateContex
|
|
|
88
102
|
fontSize: titleFontSize,
|
|
89
103
|
fontFamily: getThemeFont(context, 'title'),
|
|
90
104
|
fontWeight: 'bold',
|
|
91
|
-
color: imageSrc ?
|
|
105
|
+
color: imageSrc ? theme.colors.text : colors.text,
|
|
92
106
|
textAlign: 'center',
|
|
93
|
-
shadow:
|
|
107
|
+
shadow: shouldUseShadow(context),
|
|
94
108
|
},
|
|
95
109
|
},
|
|
96
110
|
position: {
|
|
@@ -99,7 +113,7 @@ export function sectionHeader(input: SectionHeaderInput, context: TemplateContex
|
|
|
99
113
|
anchor: 'center',
|
|
100
114
|
width: layout.maxTextWidth,
|
|
101
115
|
},
|
|
102
|
-
animation: { type: 'fadeIn', duration: 1.5 },
|
|
116
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 1.5 }),
|
|
103
117
|
});
|
|
104
118
|
|
|
105
119
|
return layers;
|
|
@@ -12,8 +12,15 @@
|
|
|
12
12
|
|
|
13
13
|
import type { Layer } from '../../schemas/Doc.js';
|
|
14
14
|
import type { StatHighlightInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
15
|
+
import {
|
|
16
|
+
resolveColorScheme,
|
|
17
|
+
getTemplateHint,
|
|
18
|
+
getThemeFont,
|
|
19
|
+
shouldUseShadow,
|
|
20
|
+
themedFontSize,
|
|
21
|
+
themedSurfaceGradient,
|
|
22
|
+
themedImageTreatment,
|
|
23
|
+
} from '../utils/themeUtils.js';
|
|
17
24
|
import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
|
|
18
25
|
import { createBackgroundLayer } from './captionUtils.js';
|
|
19
26
|
|
|
@@ -39,20 +46,21 @@ export function statHighlight(input: StatHighlightInput, context: TemplateContex
|
|
|
39
46
|
const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
|
|
40
47
|
|
|
41
48
|
// Scale font sizes — stat is dramatically large, description is understated
|
|
42
|
-
const statFontSize =
|
|
43
|
-
const descFontSize =
|
|
44
|
-
const detailFontSize =
|
|
49
|
+
const statFontSize = themedFontSize(148, context, true);
|
|
50
|
+
const descFontSize = themedFontSize(32, context, false);
|
|
51
|
+
const detailFontSize = themedFontSize(26, context, false);
|
|
45
52
|
|
|
46
|
-
const layers: Layer[] = [
|
|
47
|
-
createBackgroundLayer(
|
|
48
|
-
'bg',
|
|
49
|
-
`linear-gradient(180deg, ${theme.colors.background} 0%, #0f1520 100%)`,
|
|
50
|
-
),
|
|
51
|
-
];
|
|
53
|
+
const layers: Layer[] = [createBackgroundLayer('bg', themedSurfaceGradient(context, 180))];
|
|
52
54
|
|
|
53
55
|
// Add accent image layers (behind text, after background)
|
|
54
56
|
if (accentImage) {
|
|
55
|
-
layers.push(
|
|
57
|
+
layers.push(
|
|
58
|
+
...createAccentLayers(
|
|
59
|
+
accentImage,
|
|
60
|
+
input.id,
|
|
61
|
+
themedImageTreatment(context, input.imageTreatment),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
// Big stat — hero element, dominates the slide
|
|
@@ -66,18 +74,23 @@ export function statHighlight(input: StatHighlightInput, context: TemplateContex
|
|
|
66
74
|
fontFamily: getThemeFont(context, 'title'),
|
|
67
75
|
fontWeight: 'bold',
|
|
68
76
|
color: colors.text,
|
|
69
|
-
shadow:
|
|
77
|
+
shadow: shouldUseShadow(context),
|
|
70
78
|
},
|
|
71
79
|
},
|
|
72
80
|
position: {
|
|
73
81
|
x: accentLayout.textCenterX,
|
|
74
|
-
y: adjustY('
|
|
82
|
+
y: adjustY('36%', accentLayout),
|
|
75
83
|
anchor: 'center',
|
|
76
84
|
},
|
|
77
|
-
animation:
|
|
85
|
+
animation:
|
|
86
|
+
getTemplateHint<string>(context, 'statHighlight', 'entrance', 'subtle') === 'dramatic'
|
|
87
|
+
? { type: 'zoomIn', duration: 0.4 }
|
|
88
|
+
: { type: 'zoomIn', duration: 0.6 },
|
|
78
89
|
});
|
|
79
90
|
|
|
80
|
-
// Description — smaller and understated beneath the stat
|
|
91
|
+
// Description — smaller and understated beneath the stat. Sits close
|
|
92
|
+
// under the stat (the ~26% fixed gap it used to get read as two
|
|
93
|
+
// unrelated elements floating in the panel).
|
|
81
94
|
layers.push({
|
|
82
95
|
type: 'text',
|
|
83
96
|
id: 'description',
|
|
@@ -88,13 +101,13 @@ export function statHighlight(input: StatHighlightInput, context: TemplateContex
|
|
|
88
101
|
fontFamily: getThemeFont(context, 'body'),
|
|
89
102
|
color: theme.colors.textMuted,
|
|
90
103
|
textAlign: 'center',
|
|
91
|
-
lineHeight: 1.
|
|
92
|
-
shadow:
|
|
104
|
+
lineHeight: 1.5,
|
|
105
|
+
shadow: shouldUseShadow(context),
|
|
93
106
|
},
|
|
94
107
|
},
|
|
95
108
|
position: {
|
|
96
109
|
x: accentLayout.textCenterX,
|
|
97
|
-
y: adjustY('
|
|
110
|
+
y: adjustY('54%', accentLayout),
|
|
98
111
|
width: accentLayout.textWidth,
|
|
99
112
|
anchor: 'center',
|
|
100
113
|
},
|
|
@@ -111,14 +124,18 @@ export function statHighlight(input: StatHighlightInput, context: TemplateContex
|
|
|
111
124
|
style: {
|
|
112
125
|
fontSize: detailFontSize,
|
|
113
126
|
fontFamily: getThemeFont(context, 'body'),
|
|
114
|
-
|
|
127
|
+
// Scheme *text* rather than *accent*: accents in the built-in
|
|
128
|
+
// schemes are tuned as chart/fill colors and drop below legible
|
|
129
|
+
// contrast as body copy on light surfaces.
|
|
130
|
+
color: colors.text,
|
|
115
131
|
textAlign: 'center',
|
|
116
|
-
shadow:
|
|
132
|
+
shadow: shouldUseShadow(context),
|
|
117
133
|
},
|
|
118
134
|
},
|
|
119
135
|
position: {
|
|
120
136
|
x: accentLayout.textCenterX,
|
|
121
|
-
y: adjustY('
|
|
137
|
+
y: adjustY('66%', accentLayout),
|
|
138
|
+
width: accentLayout.textWidth,
|
|
122
139
|
anchor: 'center',
|
|
123
140
|
},
|
|
124
141
|
animation: { type: 'fadeIn', duration: 1, delay: 1 },
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Title Block Template
|
|
3
3
|
*
|
|
4
4
|
* Large title with optional subtitle for doc intros.
|
|
5
|
-
* Centered text with fade-in animations
|
|
5
|
+
* Centered text with fade-in animations, composed as one lockup:
|
|
6
|
+
* accent rule, title, and subtitle are spaced from the title's
|
|
7
|
+
* estimated height instead of fixed slots that collide on wrap.
|
|
6
8
|
* Adapts font sizes and positioning for different viewports.
|
|
7
9
|
*
|
|
8
10
|
* This is shared code used by both site and efb-app doc renderers.
|
|
@@ -10,8 +12,15 @@
|
|
|
10
12
|
|
|
11
13
|
import type { Layer } from '../../schemas/Doc.js';
|
|
12
14
|
import type { TitleBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
+
import {
|
|
16
|
+
getTemplateHint,
|
|
17
|
+
getThemeFont,
|
|
18
|
+
shouldUseShadow,
|
|
19
|
+
themedEntrance,
|
|
20
|
+
themedFontSize,
|
|
21
|
+
} from '../utils/themeUtils.js';
|
|
22
|
+
import { withAlpha } from '../../schemas/colorUtils.js';
|
|
23
|
+
import { estimateTextHeight } from './captionUtils.js';
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
26
|
* Hint schema published for the theme validator + future customizer hint UI.
|
|
@@ -24,40 +33,68 @@ export const titleBlockHintSchema = {
|
|
|
24
33
|
|
|
25
34
|
export function titleBlock(input: TitleBlockInput, context: TemplateContext): Layer[] {
|
|
26
35
|
const { title, subtitle, backgroundColor } = input;
|
|
27
|
-
const { theme, layout } = context;
|
|
36
|
+
const { theme, layout, viewport } = context;
|
|
28
37
|
|
|
29
38
|
// Scale font sizes for viewport
|
|
30
|
-
const titleFontSize =
|
|
31
|
-
const subtitleFontSize =
|
|
39
|
+
const titleFontSize = themedFontSize(96, context, true);
|
|
40
|
+
const subtitleFontSize = themedFontSize(36, context, false);
|
|
32
41
|
|
|
33
42
|
const baseBg = backgroundColor || theme.colors.primary;
|
|
34
43
|
|
|
44
|
+
// Estimate the title's rendered height so the rule and subtitle key off
|
|
45
|
+
// its real extent — fixed slots let the rule overlap the ascenders and
|
|
46
|
+
// stranded the subtitle when the title wrapped.
|
|
47
|
+
const maxWidthPx = (parseFloat(layout.maxTextWidth) / 100) * viewport.width;
|
|
48
|
+
const titleH = estimateTextHeight(title, titleFontSize, maxWidthPx, 1.15);
|
|
49
|
+
const titleYPct = subtitle ? 42 : 48;
|
|
50
|
+
const px = (v: number) => (v / viewport.height) * 100;
|
|
51
|
+
|
|
35
52
|
const layers: Layer[] = [
|
|
36
|
-
// Background —
|
|
53
|
+
// Background — theme surface with a soft radial accent tint. The tint
|
|
54
|
+
// is translucent so the theme's text color keeps its contrast with the
|
|
55
|
+
// surface; a full-strength `primary` hotspot behind the title made
|
|
56
|
+
// dark-primary light themes unreadable.
|
|
37
57
|
{
|
|
38
58
|
type: 'shape',
|
|
39
59
|
id: 'bg',
|
|
40
60
|
content: {
|
|
41
61
|
shape: 'rect',
|
|
42
|
-
fill:
|
|
62
|
+
fill: theme.colors.background,
|
|
43
63
|
},
|
|
44
64
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
45
65
|
},
|
|
46
|
-
// Subtle decorative line above title
|
|
47
66
|
{
|
|
67
|
+
type: 'shape',
|
|
68
|
+
id: 'bg-tint',
|
|
69
|
+
content: {
|
|
70
|
+
shape: 'rect',
|
|
71
|
+
fill: `radial-gradient(ellipse at 50% 40%, ${withAlpha(baseBg, 0.32)} 0%, ${withAlpha(baseBg, 0)} 75%)`,
|
|
72
|
+
},
|
|
73
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Subtle decorative line, clear above the title's ascenders. Themes can
|
|
78
|
+
// hide it via `templateHints.title.showAccentLine: false` (the line is
|
|
79
|
+
// decorative; title/subtitle positions don't depend on it).
|
|
80
|
+
if (getTemplateHint(context, 'title', 'showAccentLine', true)) {
|
|
81
|
+
layers.push({
|
|
48
82
|
type: 'shape',
|
|
49
83
|
id: 'accent-line',
|
|
50
84
|
content: {
|
|
51
85
|
shape: 'rect',
|
|
52
|
-
fill:
|
|
86
|
+
fill: withAlpha(theme.colors.text, 0.3),
|
|
53
87
|
},
|
|
54
88
|
position: {
|
|
55
|
-
x: '
|
|
56
|
-
y:
|
|
57
|
-
width: '
|
|
58
|
-
height: '
|
|
89
|
+
x: '44%',
|
|
90
|
+
y: `${titleYPct - px(titleH / 2 + 56)}%`,
|
|
91
|
+
width: '12%',
|
|
92
|
+
height: '3px',
|
|
59
93
|
},
|
|
60
|
-
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
layers.push(
|
|
61
98
|
// Title
|
|
62
99
|
{
|
|
63
100
|
type: 'text',
|
|
@@ -70,21 +107,22 @@ export function titleBlock(input: TitleBlockInput, context: TemplateContext): La
|
|
|
70
107
|
fontWeight: 'bold',
|
|
71
108
|
color: theme.colors.text,
|
|
72
109
|
textAlign: 'center',
|
|
73
|
-
shadow:
|
|
110
|
+
shadow: shouldUseShadow(context),
|
|
74
111
|
},
|
|
75
112
|
},
|
|
76
113
|
position: {
|
|
77
114
|
x: '50%',
|
|
78
|
-
y:
|
|
115
|
+
y: `${titleYPct}%`,
|
|
79
116
|
anchor: 'center',
|
|
80
117
|
width: layout.maxTextWidth,
|
|
81
118
|
},
|
|
82
|
-
animation: { type: 'fadeIn', duration: 2 },
|
|
119
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 2 }),
|
|
83
120
|
},
|
|
84
|
-
|
|
121
|
+
);
|
|
85
122
|
|
|
86
|
-
// Add subtitle if provided
|
|
123
|
+
// Add subtitle if provided — hangs from the title's estimated bottom edge
|
|
87
124
|
if (subtitle) {
|
|
125
|
+
const subH = estimateTextHeight(subtitle, subtitleFontSize, maxWidthPx, 1.5);
|
|
88
126
|
layers.push({
|
|
89
127
|
type: 'text',
|
|
90
128
|
id: 'subtitle',
|
|
@@ -100,7 +138,7 @@ export function titleBlock(input: TitleBlockInput, context: TemplateContext): La
|
|
|
100
138
|
},
|
|
101
139
|
position: {
|
|
102
140
|
x: '50%',
|
|
103
|
-
y:
|
|
141
|
+
y: `${titleYPct + px(titleH / 2 + 56 + subH / 2)}%`,
|
|
104
142
|
anchor: 'center',
|
|
105
143
|
width: layout.maxTextWidth,
|
|
106
144
|
},
|