@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveTokens — placeholder substitution unit tests.
|
|
3
|
+
*
|
|
4
|
+
* Covers each supported token, the escape syntax, and the edge cases
|
|
5
|
+
* called out in the plan (missing data, out-of-range image index,
|
|
6
|
+
* unknown token left literal).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import { resolveTokens } from '../resolveTokens';
|
|
11
|
+
import type { Block, TextLayer, ImageLayer } from '../../../../schemas/Doc.js';
|
|
12
|
+
|
|
13
|
+
function textLayer(id: string, text: string): TextLayer {
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
type: 'text',
|
|
17
|
+
position: { x: 0, y: 0, width: 100, height: 50 },
|
|
18
|
+
content: { text, style: { fontSize: 24, color: '#000' } },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function imageLayer(id: string, src: string, alt = ''): ImageLayer {
|
|
23
|
+
return {
|
|
24
|
+
id,
|
|
25
|
+
type: 'image',
|
|
26
|
+
position: { x: 0, y: 0, width: 100, height: 100 },
|
|
27
|
+
content: { src, alt },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function block(overrides: Partial<Block> = {}): Block {
|
|
32
|
+
return {
|
|
33
|
+
id: 'b',
|
|
34
|
+
startTime: 0,
|
|
35
|
+
duration: 1,
|
|
36
|
+
audioSegment: 0,
|
|
37
|
+
title: 'Block title',
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('resolveTokens — TextLayer', () => {
|
|
43
|
+
it('substitutes {title} with block.title', () => {
|
|
44
|
+
const layers = resolveTokens([textLayer('a', 'Hello, {title}!')], block({ title: 'World' }));
|
|
45
|
+
const out = layers[0] as TextLayer;
|
|
46
|
+
expect(out.content.text).toBe('Hello, World!');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('collapses {title} to empty when block has no title', () => {
|
|
50
|
+
const layers = resolveTokens([textLayer('a', '[{title}]')], block({ title: undefined }));
|
|
51
|
+
expect((layers[0] as TextLayer).content.text).toBe('[]');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('substitutes {content} from block.contents (plain text of body)', () => {
|
|
55
|
+
const layers = resolveTokens(
|
|
56
|
+
[textLayer('a', 'Body: {content}')],
|
|
57
|
+
block({
|
|
58
|
+
contents: [
|
|
59
|
+
{
|
|
60
|
+
type: 'paragraph',
|
|
61
|
+
children: [{ type: 'text', value: 'Hello there' }],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
expect((layers[0] as TextLayer).content.text).toBe('Body: Hello there');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('joins multiple paragraphs in {content} with single spaces', () => {
|
|
70
|
+
const layers = resolveTokens(
|
|
71
|
+
[textLayer('a', '{content}')],
|
|
72
|
+
block({
|
|
73
|
+
contents: [
|
|
74
|
+
{ type: 'paragraph', children: [{ type: 'text', value: 'First.' }] },
|
|
75
|
+
{ type: 'paragraph', children: [{ type: 'text', value: 'Second.' }] },
|
|
76
|
+
],
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
expect((layers[0] as TextLayer).content.text).toBe('First. Second.');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('joins child titles for {children}', () => {
|
|
83
|
+
const layers = resolveTokens(
|
|
84
|
+
[textLayer('a', 'Sections: {children}')],
|
|
85
|
+
block({
|
|
86
|
+
children: [
|
|
87
|
+
block({ id: 'c1', title: 'Alpha' }),
|
|
88
|
+
block({ id: 'c2', title: 'Beta' }),
|
|
89
|
+
block({ id: 'c3', title: 'Gamma' }),
|
|
90
|
+
],
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
expect((layers[0] as TextLayer).content.text).toBe('Sections: Alpha, Beta, Gamma');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('substitutes {image:N} with the Nth image alt text', () => {
|
|
97
|
+
const layers = resolveTokens(
|
|
98
|
+
[textLayer('a', 'Caption: {image:0}')],
|
|
99
|
+
block({
|
|
100
|
+
contents: [
|
|
101
|
+
{
|
|
102
|
+
type: 'paragraph',
|
|
103
|
+
children: [{ type: 'image', url: '/a.png', alt: 'Alpha image' }],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
expect((layers[0] as TextLayer).content.text).toBe('Caption: Alpha image');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('preserves {{title}} as literal {title}', () => {
|
|
112
|
+
const layers = resolveTokens(
|
|
113
|
+
[textLayer('a', 'Use {{title}} as a token, or {title} to expand')],
|
|
114
|
+
block({ title: 'My Doc' }),
|
|
115
|
+
);
|
|
116
|
+
expect((layers[0] as TextLayer).content.text).toBe(
|
|
117
|
+
'Use {title} as a token, or My Doc to expand',
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('leaves unknown tokens literal', () => {
|
|
122
|
+
const layers = resolveTokens([textLayer('a', 'Hello {nope} world')], block());
|
|
123
|
+
expect((layers[0] as TextLayer).content.text).toBe('Hello {nope} world');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('substitutes multiple tokens in a single string', () => {
|
|
127
|
+
const layers = resolveTokens(
|
|
128
|
+
[textLayer('a', '{title} — {content}')],
|
|
129
|
+
block({
|
|
130
|
+
title: 'Doc',
|
|
131
|
+
contents: [{ type: 'paragraph', children: [{ type: 'text', value: 'body' }] }],
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
expect((layers[0] as TextLayer).content.text).toBe('Doc — body');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('resolveTokens — ImageLayer', () => {
|
|
139
|
+
it('substitutes {image:0} with the Nth image URL in src', () => {
|
|
140
|
+
const layers = resolveTokens(
|
|
141
|
+
[imageLayer('a', '{image:0}')],
|
|
142
|
+
block({
|
|
143
|
+
contents: [
|
|
144
|
+
{
|
|
145
|
+
type: 'paragraph',
|
|
146
|
+
children: [{ type: 'image', url: '/hero.png', alt: 'hero' }],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
expect(layers).toHaveLength(1);
|
|
152
|
+
expect((layers[0] as ImageLayer).content.src).toBe('/hero.png');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('drops the layer entirely when the requested image index is out of range', () => {
|
|
156
|
+
const layers = resolveTokens(
|
|
157
|
+
[imageLayer('a', '{image:99}'), imageLayer('b', '/keep.png')],
|
|
158
|
+
block({
|
|
159
|
+
contents: [
|
|
160
|
+
{
|
|
161
|
+
type: 'paragraph',
|
|
162
|
+
children: [{ type: 'image', url: '/one.png', alt: '' }],
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
expect(layers).toHaveLength(1);
|
|
168
|
+
expect(layers[0].id).toBe('b');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('substitutes tokens in the alt text when the src is also a token', () => {
|
|
172
|
+
const layers = resolveTokens(
|
|
173
|
+
[imageLayer('a', '{image:0}', 'Image for {title}')],
|
|
174
|
+
block({
|
|
175
|
+
title: 'My Doc',
|
|
176
|
+
contents: [
|
|
177
|
+
{
|
|
178
|
+
type: 'paragraph',
|
|
179
|
+
children: [{ type: 'image', url: '/x.png', alt: '' }],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
const img = layers[0] as ImageLayer;
|
|
185
|
+
expect(img.content.src).toBe('/x.png');
|
|
186
|
+
expect(img.content.alt).toBe('Image for My Doc');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('passes through ImageLayer with no token in src', () => {
|
|
190
|
+
const layers = resolveTokens([imageLayer('a', '/static.png')], block());
|
|
191
|
+
expect((layers[0] as ImageLayer).content.src).toBe('/static.png');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('resolveTokens — other layer types', () => {
|
|
196
|
+
it('passes ShapeLayer / PathLayer through unchanged', () => {
|
|
197
|
+
const shape = {
|
|
198
|
+
id: 's',
|
|
199
|
+
type: 'shape' as const,
|
|
200
|
+
position: { x: 0, y: 0, width: 50, height: 50 },
|
|
201
|
+
content: { shape: 'rect' as const, fill: '#fff' },
|
|
202
|
+
};
|
|
203
|
+
const path = {
|
|
204
|
+
id: 'p',
|
|
205
|
+
type: 'path' as const,
|
|
206
|
+
position: { x: 0, y: 0, width: 100, height: 100 },
|
|
207
|
+
content: { d: 'M 0 0 L 100 100', stroke: '#000' },
|
|
208
|
+
};
|
|
209
|
+
const layers = resolveTokens([shape, path], block());
|
|
210
|
+
expect(layers).toEqual([shape, path]);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('resolveTokens — purity', () => {
|
|
215
|
+
it('does not mutate the input layers array or the layers themselves', () => {
|
|
216
|
+
const original = textLayer('a', '{title}');
|
|
217
|
+
const before = JSON.stringify(original);
|
|
218
|
+
resolveTokens([original], block({ title: 'X' }));
|
|
219
|
+
expect(JSON.stringify(original)).toBe(before);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
Binary file
|
|
@@ -12,8 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
import type { Layer } from '../../schemas/Doc.js';
|
|
14
14
|
import type { TwoColumnInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
15
|
+
import {
|
|
16
|
+
resolveColorScheme,
|
|
17
|
+
getThemeFont,
|
|
18
|
+
themedEntrance,
|
|
19
|
+
themedFontSize,
|
|
20
|
+
themedSurfaceGradient,
|
|
21
|
+
} from '../utils/themeUtils.js';
|
|
22
|
+
import { withAlpha } from '../../schemas/colorUtils.js';
|
|
17
23
|
import { getTwoColumnPositions } from '../../schemas/LayoutStrategy.js';
|
|
18
24
|
|
|
19
25
|
export function twoColumn(input: TwoColumnInput, context: TemplateContext): Layer[] {
|
|
@@ -33,10 +39,17 @@ export function twoColumn(input: TwoColumnInput, context: TemplateContext): Laye
|
|
|
33
39
|
// Column width ~42% for side-by-side, ~85% for stacked
|
|
34
40
|
const columnWidth = isStacked ? '85%' : '42%';
|
|
35
41
|
|
|
36
|
-
// Calculate font sizes based on viewport
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
// Calculate font sizes based on viewport. The header is the block's
|
|
43
|
+
// title, so it sits a step ABOVE the column labels in the hierarchy —
|
|
44
|
+
// it used to render smaller and dimmer than the labels, which read as
|
|
45
|
+
// an inverted hierarchy.
|
|
46
|
+
const headerFontSize = themedFontSize(36, context, true);
|
|
47
|
+
const labelFontSize = themedFontSize(44, context, true);
|
|
48
|
+
const sublabelFontSize = themedFontSize(22, context, false);
|
|
49
|
+
|
|
50
|
+
// Panels drop below the header band so the title never overlaps them.
|
|
51
|
+
const panelTop = header && !isStacked ? 22 : 15;
|
|
52
|
+
const panelHeight = header && !isStacked ? 64 : 70;
|
|
40
53
|
|
|
41
54
|
const layers: Layer[] = [
|
|
42
55
|
// Background — subtle gradient to add depth
|
|
@@ -45,38 +58,41 @@ export function twoColumn(input: TwoColumnInput, context: TemplateContext): Laye
|
|
|
45
58
|
id: 'bg',
|
|
46
59
|
content: {
|
|
47
60
|
shape: 'rect',
|
|
48
|
-
fill:
|
|
61
|
+
fill: themedSurfaceGradient(context, 135),
|
|
49
62
|
},
|
|
50
63
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
51
64
|
},
|
|
52
65
|
];
|
|
53
66
|
|
|
54
67
|
if (!isStacked) {
|
|
55
|
-
//
|
|
68
|
+
// Column background panels — tinted from the theme text color so
|
|
69
|
+
// they stay visible on light and dark surfaces alike.
|
|
70
|
+
const panelFill = withAlpha(theme.colors.text, 0.05);
|
|
56
71
|
layers.push({
|
|
57
72
|
type: 'shape',
|
|
58
73
|
id: 'left-panel',
|
|
59
74
|
content: {
|
|
60
75
|
shape: 'rect',
|
|
61
|
-
fill:
|
|
76
|
+
fill: panelFill,
|
|
77
|
+
borderRadius: 12,
|
|
62
78
|
},
|
|
63
|
-
position: { x: '3%', y:
|
|
79
|
+
position: { x: '3%', y: `${panelTop}%`, width: '44%', height: `${panelHeight}%` },
|
|
64
80
|
});
|
|
65
81
|
|
|
66
|
-
// Right column background panel
|
|
67
82
|
layers.push({
|
|
68
83
|
type: 'shape',
|
|
69
84
|
id: 'right-panel',
|
|
70
85
|
content: {
|
|
71
86
|
shape: 'rect',
|
|
72
|
-
fill:
|
|
87
|
+
fill: panelFill,
|
|
88
|
+
borderRadius: 12,
|
|
73
89
|
},
|
|
74
|
-
position: { x: '53%', y:
|
|
90
|
+
position: { x: '53%', y: `${panelTop}%`, width: '44%', height: `${panelHeight}%` },
|
|
75
91
|
});
|
|
76
92
|
}
|
|
77
93
|
|
|
78
|
-
// Header position adjusts based on layout
|
|
79
|
-
const headerY = isStacked ? '
|
|
94
|
+
// Header position adjusts based on layout — always clear of the panels.
|
|
95
|
+
const headerY = isStacked ? '8%' : '12%';
|
|
80
96
|
|
|
81
97
|
// Add header if provided
|
|
82
98
|
if (header) {
|
|
@@ -88,12 +104,13 @@ export function twoColumn(input: TwoColumnInput, context: TemplateContext): Laye
|
|
|
88
104
|
style: {
|
|
89
105
|
fontSize: headerFontSize,
|
|
90
106
|
fontFamily: getThemeFont(context, 'title'),
|
|
91
|
-
|
|
107
|
+
fontWeight: 'bold',
|
|
108
|
+
color: theme.colors.text,
|
|
92
109
|
textAlign: 'center',
|
|
93
110
|
},
|
|
94
111
|
},
|
|
95
112
|
position: { x: '50%', y: headerY, anchor: 'center' },
|
|
96
|
-
animation: { type: 'fadeIn', duration: 0.8 },
|
|
113
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 0.8 }),
|
|
97
114
|
});
|
|
98
115
|
}
|
|
99
116
|
|
|
@@ -110,13 +127,12 @@ export function twoColumn(input: TwoColumnInput, context: TemplateContext): Laye
|
|
|
110
127
|
rightY = header ? '60%' : '55%';
|
|
111
128
|
rightSublabelY = header ? '70%' : '65%';
|
|
112
129
|
} else {
|
|
113
|
-
// Landscape/square: side by side — vertically centered in panels
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
rightSublabelY = sublabelOffset;
|
|
130
|
+
// Landscape/square: side by side — vertically centered in the panels.
|
|
131
|
+
const panelCenter = panelTop + panelHeight / 2;
|
|
132
|
+
leftY = `${panelCenter - 4}%`;
|
|
133
|
+
leftSublabelY = `${panelCenter + 7}%`;
|
|
134
|
+
rightY = `${panelCenter - 4}%`;
|
|
135
|
+
rightSublabelY = `${panelCenter + 7}%`;
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
// Left column (or top in portrait)
|
|
@@ -140,7 +156,7 @@ export function twoColumn(input: TwoColumnInput, context: TemplateContext): Laye
|
|
|
140
156
|
anchor: 'center',
|
|
141
157
|
width: columnWidth,
|
|
142
158
|
},
|
|
143
|
-
animation: { type: 'fadeIn', duration: 1 },
|
|
159
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 1 }),
|
|
144
160
|
});
|
|
145
161
|
|
|
146
162
|
if (left.sublabel) {
|
|
@@ -167,9 +183,11 @@ export function twoColumn(input: TwoColumnInput, context: TemplateContext): Laye
|
|
|
167
183
|
});
|
|
168
184
|
}
|
|
169
185
|
|
|
170
|
-
// Visual connector between columns
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
// Visual connector between columns — sized to fit inside the gutter
|
|
187
|
+
// between the panels (a 120px glyph used to cross both panel edges),
|
|
188
|
+
// tinted from the theme text color so it shows on any surface.
|
|
189
|
+
const connectorFontSize = themedFontSize(64, context, true);
|
|
190
|
+
const connectorY = isStacked ? '48%' : `${panelTop + panelHeight / 2}%`;
|
|
173
191
|
const connectorSymbol = isStacked ? '\u2193' : '\u2192'; // ↓ for stacked, → for side-by-side
|
|
174
192
|
|
|
175
193
|
layers.push({
|
|
@@ -181,7 +199,7 @@ export function twoColumn(input: TwoColumnInput, context: TemplateContext): Laye
|
|
|
181
199
|
fontSize: connectorFontSize,
|
|
182
200
|
fontFamily: getThemeFont(context, 'title'),
|
|
183
201
|
fontWeight: 'bold',
|
|
184
|
-
color:
|
|
202
|
+
color: withAlpha(theme.colors.text, 0.45),
|
|
185
203
|
textAlign: 'center',
|
|
186
204
|
},
|
|
187
205
|
},
|
|
@@ -214,7 +232,7 @@ export function twoColumn(input: TwoColumnInput, context: TemplateContext): Laye
|
|
|
214
232
|
anchor: 'center',
|
|
215
233
|
width: columnWidth,
|
|
216
234
|
},
|
|
217
|
-
animation: { type: 'fadeIn', duration: 1, delay: 0.5 },
|
|
235
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 1, delay: 0.5 }),
|
|
218
236
|
});
|
|
219
237
|
|
|
220
238
|
if (right.sublabel) {
|
|
@@ -17,18 +17,21 @@
|
|
|
17
17
|
|
|
18
18
|
import type { Layer } from '../../schemas/Doc.js';
|
|
19
19
|
import type { VideoPullQuoteInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { getThemeFont, themedEntrance, themedFontSize } from '../utils/themeUtils.js';
|
|
21
|
+
import { estimateTextHeight } from './captionUtils.js';
|
|
22
22
|
|
|
23
23
|
export function videoPullQuote(input: VideoPullQuoteInput, context: TemplateContext): Layer[] {
|
|
24
24
|
const { text, attribution, backgroundVideo } = input;
|
|
25
|
+
const { viewport } = context;
|
|
25
26
|
|
|
26
27
|
// Guard: backgroundVideo is required
|
|
27
28
|
if (!backgroundVideo?.src) return [];
|
|
28
29
|
|
|
29
|
-
const quoteFontSize =
|
|
30
|
-
const attrFontSize =
|
|
31
|
-
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;
|
|
32
35
|
|
|
33
36
|
const layers: Layer[] = [
|
|
34
37
|
// Full-bleed background video clip
|
|
@@ -90,22 +93,29 @@ export function videoPullQuote(input: VideoPullQuoteInput, context: TemplateCont
|
|
|
90
93
|
fontFamily: getThemeFont(context, 'title'),
|
|
91
94
|
color: '#ffffff',
|
|
92
95
|
textAlign: 'center',
|
|
93
|
-
lineHeight:
|
|
96
|
+
lineHeight: quoteLineHeight,
|
|
94
97
|
shadow: true,
|
|
95
98
|
},
|
|
96
99
|
},
|
|
97
100
|
position: {
|
|
98
101
|
x: '50%',
|
|
99
|
-
y:
|
|
102
|
+
y: `${quoteYPct}%`,
|
|
100
103
|
anchor: 'center',
|
|
101
|
-
width: '
|
|
104
|
+
width: '72%',
|
|
102
105
|
},
|
|
103
|
-
animation: { type: 'fadeIn', duration: 2 },
|
|
106
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 2 }),
|
|
104
107
|
},
|
|
105
108
|
];
|
|
106
109
|
|
|
107
|
-
// Attribution
|
|
110
|
+
// Attribution \u2014 hangs just below the quote's estimated bottom edge so
|
|
111
|
+
// the two read as one lockup instead of stranding near the bottom.
|
|
108
112
|
if (attribution) {
|
|
113
|
+
const quoteWidthPx = 0.72 * viewport.width;
|
|
114
|
+
const quoteHeightPx = estimateTextHeight(text, quoteFontSize, quoteWidthPx, quoteLineHeight);
|
|
115
|
+
const attrYPct = Math.min(
|
|
116
|
+
82,
|
|
117
|
+
quoteYPct + ((quoteHeightPx / 2 + quoteFontSize * 1.7) / viewport.height) * 100,
|
|
118
|
+
);
|
|
109
119
|
layers.push({
|
|
110
120
|
type: 'text',
|
|
111
121
|
id: 'attribution',
|
|
@@ -114,14 +124,14 @@ export function videoPullQuote(input: VideoPullQuoteInput, context: TemplateCont
|
|
|
114
124
|
style: {
|
|
115
125
|
fontSize: attrFontSize,
|
|
116
126
|
fontFamily: getThemeFont(context, 'body'),
|
|
117
|
-
color: 'rgba(255, 255, 255, 0.
|
|
127
|
+
color: 'rgba(255, 255, 255, 0.85)',
|
|
118
128
|
textAlign: 'center',
|
|
119
129
|
shadow: true,
|
|
120
130
|
},
|
|
121
131
|
},
|
|
122
132
|
position: {
|
|
123
133
|
x: '50%',
|
|
124
|
-
y:
|
|
134
|
+
y: `${attrYPct}%`,
|
|
125
135
|
anchor: 'center',
|
|
126
136
|
},
|
|
127
137
|
animation: { type: 'fadeIn', duration: 1, delay: 1.5 },
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import type { Layer } from '../../schemas/Doc.js';
|
|
19
19
|
import type { VideoWithCaptionInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { getThemeFont, shouldUseShadow, themedFontSize } from '../utils/themeUtils.js';
|
|
21
|
+
import { withAlpha } from '../../schemas/colorUtils.js';
|
|
22
22
|
import { cleanCaption } from './captionUtils.js';
|
|
23
23
|
|
|
24
24
|
export function videoWithCaption(input: VideoWithCaptionInput, context: TemplateContext): Layer[] {
|
|
@@ -37,8 +37,8 @@ export function videoWithCaption(input: VideoWithCaptionInput, context: Template
|
|
|
37
37
|
const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
|
|
38
38
|
const { theme, layout } = context;
|
|
39
39
|
|
|
40
|
-
const captionFontSize =
|
|
41
|
-
const creditFontSize =
|
|
40
|
+
const captionFontSize = themedFontSize(36, context, false);
|
|
41
|
+
const creditFontSize = themedFontSize(16, context, false);
|
|
42
42
|
|
|
43
43
|
const layers: Layer[] = [
|
|
44
44
|
// Background video clip
|
|
@@ -60,14 +60,19 @@ export function videoWithCaption(input: VideoWithCaptionInput, context: Template
|
|
|
60
60
|
},
|
|
61
61
|
];
|
|
62
62
|
|
|
63
|
-
// Caption text at bottom with gradient for readability
|
|
63
|
+
// Caption text at bottom with gradient for readability. The scrim is
|
|
64
|
+
// tinted from the theme background so the theme text color on top of it
|
|
65
|
+
// stays readable in light and dark themes alike; it fades toward the
|
|
66
|
+
// frame edge the caption sits on.
|
|
64
67
|
if (caption) {
|
|
68
|
+
const bg = theme.colors.background;
|
|
69
|
+
const scrimAngle = captionPosition === 'top' ? 180 : 0;
|
|
65
70
|
layers.push({
|
|
66
71
|
type: 'shape',
|
|
67
72
|
id: 'caption-gradient',
|
|
68
73
|
content: {
|
|
69
74
|
shape: 'rect',
|
|
70
|
-
fill:
|
|
75
|
+
fill: `linear-gradient(${scrimAngle}deg, ${withAlpha(bg, 0.8)} 0%, ${withAlpha(bg, 0.35)} 60%, ${withAlpha(bg, 0)} 100%)`,
|
|
71
76
|
},
|
|
72
77
|
position: {
|
|
73
78
|
x: 0,
|
|
@@ -90,7 +95,7 @@ export function videoWithCaption(input: VideoWithCaptionInput, context: Template
|
|
|
90
95
|
fontFamily: getThemeFont(context, 'body'),
|
|
91
96
|
color: theme.colors.text,
|
|
92
97
|
textAlign: 'center',
|
|
93
|
-
shadow:
|
|
98
|
+
shadow: shouldUseShadow(context),
|
|
94
99
|
},
|
|
95
100
|
},
|
|
96
101
|
position: {
|
|
@@ -113,7 +118,7 @@ export function videoWithCaption(input: VideoWithCaptionInput, context: Template
|
|
|
113
118
|
style: {
|
|
114
119
|
fontSize: creditFontSize,
|
|
115
120
|
fontFamily: getThemeFont(context, 'body'),
|
|
116
|
-
color:
|
|
121
|
+
color: withAlpha(theme.colors.text, 0.65),
|
|
117
122
|
textAlign: 'right',
|
|
118
123
|
},
|
|
119
124
|
},
|