@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,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry merge — end-to-end: a doc that uses a custom template name
|
|
3
|
+
* resolves through `expandDocBlocks(opts.customTemplates)` and produces
|
|
4
|
+
* layers with tokens substituted from the source block.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { buildRegistry, expandDocBlocks } from '../index';
|
|
9
|
+
import type { CustomTemplateDefinition } from '../../../schemas/CustomTemplates.js';
|
|
10
|
+
import type { Block, TextLayer } from '../../../schemas/Doc.js';
|
|
11
|
+
|
|
12
|
+
const heroDef: CustomTemplateDefinition = {
|
|
13
|
+
name: 'hero',
|
|
14
|
+
label: 'Hero',
|
|
15
|
+
viewport: { width: 1920, height: 1080 },
|
|
16
|
+
layers: [
|
|
17
|
+
{
|
|
18
|
+
id: 'title',
|
|
19
|
+
type: 'text',
|
|
20
|
+
position: { x: '5%', y: '10%', width: '90%' },
|
|
21
|
+
content: { text: '{title}', style: { fontSize: 72, color: '#000' } },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'body',
|
|
25
|
+
type: 'text',
|
|
26
|
+
position: { x: '5%', y: '40%', width: '90%' },
|
|
27
|
+
content: { text: '{content}', style: { fontSize: 32, color: '#333' } },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe('buildRegistry', () => {
|
|
33
|
+
it('merges custom templates onto the built-in registry', () => {
|
|
34
|
+
const reg = buildRegistry([heroDef]);
|
|
35
|
+
expect(reg.hero).toBeDefined();
|
|
36
|
+
expect(reg.title).toBeDefined(); // built-in survives
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does not let custom templates shadow built-in names', () => {
|
|
40
|
+
const collide: CustomTemplateDefinition = {
|
|
41
|
+
...heroDef,
|
|
42
|
+
name: 'title', // collides with built-in
|
|
43
|
+
};
|
|
44
|
+
const reg = buildRegistry([collide]);
|
|
45
|
+
// Built-in `title` is preserved — the collide entry is silently
|
|
46
|
+
// dropped (callers shouldn't have created it). We verify by
|
|
47
|
+
// checking the function reference is the original built-in's.
|
|
48
|
+
const builtIn = buildRegistry();
|
|
49
|
+
expect(reg.title).toBe(builtIn.title);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('expandDocBlocks with customTemplates', () => {
|
|
54
|
+
it('expands a doc-defined template name into resolved layers', () => {
|
|
55
|
+
const block: Block = {
|
|
56
|
+
id: 'b1',
|
|
57
|
+
startTime: 0,
|
|
58
|
+
duration: 1,
|
|
59
|
+
audioSegment: 0,
|
|
60
|
+
title: 'Welcome',
|
|
61
|
+
template: 'hero',
|
|
62
|
+
contents: [
|
|
63
|
+
{
|
|
64
|
+
type: 'paragraph',
|
|
65
|
+
children: [{ type: 'text', value: 'A small editor for big ideas.' }],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const expanded = expandDocBlocks([block], { customTemplates: [heroDef] });
|
|
71
|
+
expect(expanded).toHaveLength(1);
|
|
72
|
+
const layers = expanded[0].layers ?? [];
|
|
73
|
+
expect(layers).toHaveLength(2);
|
|
74
|
+
expect((layers[0] as TextLayer).content.text).toBe('Welcome');
|
|
75
|
+
expect((layers[1] as TextLayer).content.text).toBe('A small editor for big ideas.');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('preserves %-based positions so the SSR renderer can resolve them per viewport', () => {
|
|
79
|
+
const block: Block = {
|
|
80
|
+
id: 'b1',
|
|
81
|
+
startTime: 0,
|
|
82
|
+
duration: 1,
|
|
83
|
+
audioSegment: 0,
|
|
84
|
+
title: 'X',
|
|
85
|
+
template: 'hero',
|
|
86
|
+
};
|
|
87
|
+
const expanded = expandDocBlocks([block], { customTemplates: [heroDef] });
|
|
88
|
+
expect(expanded[0].layers![0].position.x).toBe('5%');
|
|
89
|
+
expect(expanded[0].layers![0].position.width).toBe('90%');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('falls back to a no-layer block (with a warning) when the template is unknown', () => {
|
|
93
|
+
// Suppress the expected console.warn from `expandTemplateBlock`.
|
|
94
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
95
|
+
const block: Block = {
|
|
96
|
+
id: 'b1',
|
|
97
|
+
startTime: 0,
|
|
98
|
+
duration: 1,
|
|
99
|
+
audioSegment: 0,
|
|
100
|
+
title: 'Y',
|
|
101
|
+
template: 'nonexistent',
|
|
102
|
+
};
|
|
103
|
+
const expanded = expandDocBlocks([block], { customTemplates: [heroDef] });
|
|
104
|
+
expect(expanded[0].layers).toBeUndefined();
|
|
105
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
106
|
+
warnSpy.mockRestore();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
import { vi } from 'vitest';
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
* This is shared code used by both site and efb-app doc renderers.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
Layer,
|
|
18
|
+
ImageLayer,
|
|
19
|
+
ImageTreatment,
|
|
20
|
+
ShapeLayer,
|
|
21
|
+
Animation,
|
|
22
|
+
} from '../../schemas/Doc.js';
|
|
17
23
|
import type { AccentImage, AccentPosition } from '../../schemas/BlockTemplates.js';
|
|
18
24
|
|
|
19
25
|
const PERCENTAGE_RE = /^(\d+(?:\.\d+)?)\s*%?$/;
|
|
@@ -125,28 +131,40 @@ export function getAccentLayout(position: AccentPosition): AccentLayout {
|
|
|
125
131
|
* Create layers for an accent image.
|
|
126
132
|
* Returns the image layer and any overlay/gradient layers needed.
|
|
127
133
|
*/
|
|
128
|
-
export function createAccentLayers(
|
|
134
|
+
export function createAccentLayers(
|
|
135
|
+
accent: AccentImage,
|
|
136
|
+
slideId: string,
|
|
137
|
+
treatment?: ImageTreatment,
|
|
138
|
+
): Layer[] {
|
|
129
139
|
const layers: Layer[] = [];
|
|
130
140
|
const { src, alt, position, ambientMotion, credit, license } = accent;
|
|
131
141
|
|
|
132
142
|
switch (position) {
|
|
133
143
|
case 'left-strip':
|
|
134
|
-
layers.push(
|
|
144
|
+
layers.push(
|
|
145
|
+
createStripImage(src, alt, 'left', slideId, ambientMotion, credit, license, treatment),
|
|
146
|
+
);
|
|
135
147
|
layers.push(createStripGradient('left', slideId));
|
|
136
148
|
break;
|
|
137
149
|
|
|
138
150
|
case 'right-strip':
|
|
139
|
-
layers.push(
|
|
151
|
+
layers.push(
|
|
152
|
+
createStripImage(src, alt, 'right', slideId, ambientMotion, credit, license, treatment),
|
|
153
|
+
);
|
|
140
154
|
layers.push(createStripGradient('right', slideId));
|
|
141
155
|
break;
|
|
142
156
|
|
|
143
157
|
case 'bottom-strip':
|
|
144
|
-
layers.push(
|
|
158
|
+
layers.push(
|
|
159
|
+
createBottomStripImage(src, alt, slideId, ambientMotion, credit, license, treatment),
|
|
160
|
+
);
|
|
145
161
|
layers.push(createBottomStripGradient(slideId));
|
|
146
162
|
break;
|
|
147
163
|
|
|
148
164
|
case 'corner-inset':
|
|
149
|
-
layers.push(
|
|
165
|
+
layers.push(
|
|
166
|
+
createCornerInsetImage(src, alt, slideId, ambientMotion, credit, license, treatment),
|
|
167
|
+
);
|
|
150
168
|
layers.push(createCornerVignette(slideId));
|
|
151
169
|
break;
|
|
152
170
|
}
|
|
@@ -165,6 +183,7 @@ function createStripImage(
|
|
|
165
183
|
ambientMotion?: AccentImage['ambientMotion'],
|
|
166
184
|
credit?: string,
|
|
167
185
|
license?: string,
|
|
186
|
+
treatment?: ImageTreatment,
|
|
168
187
|
): ImageLayer {
|
|
169
188
|
return {
|
|
170
189
|
type: 'image',
|
|
@@ -175,6 +194,7 @@ function createStripImage(
|
|
|
175
194
|
fit: 'cover',
|
|
176
195
|
credit,
|
|
177
196
|
license,
|
|
197
|
+
...(treatment ? { treatment } : {}),
|
|
178
198
|
},
|
|
179
199
|
position: {
|
|
180
200
|
x: side === 'left' ? 0 : `${100 - STRIP_SIZE}%`,
|
|
@@ -221,6 +241,7 @@ function createBottomStripImage(
|
|
|
221
241
|
ambientMotion?: AccentImage['ambientMotion'],
|
|
222
242
|
credit?: string,
|
|
223
243
|
license?: string,
|
|
244
|
+
treatment?: ImageTreatment,
|
|
224
245
|
): ImageLayer {
|
|
225
246
|
return {
|
|
226
247
|
type: 'image',
|
|
@@ -231,6 +252,7 @@ function createBottomStripImage(
|
|
|
231
252
|
fit: 'cover',
|
|
232
253
|
credit,
|
|
233
254
|
license,
|
|
255
|
+
...(treatment ? { treatment } : {}),
|
|
234
256
|
},
|
|
235
257
|
position: {
|
|
236
258
|
x: 0,
|
|
@@ -272,6 +294,7 @@ function createCornerInsetImage(
|
|
|
272
294
|
ambientMotion?: AccentImage['ambientMotion'],
|
|
273
295
|
credit?: string,
|
|
274
296
|
license?: string,
|
|
297
|
+
treatment?: ImageTreatment,
|
|
275
298
|
): ImageLayer {
|
|
276
299
|
return {
|
|
277
300
|
type: 'image',
|
|
@@ -282,6 +305,7 @@ function createCornerInsetImage(
|
|
|
282
305
|
fit: 'cover',
|
|
283
306
|
credit,
|
|
284
307
|
license,
|
|
308
|
+
...(treatment ? { treatment } : {}),
|
|
285
309
|
},
|
|
286
310
|
position: {
|
|
287
311
|
x: '70%',
|
|
@@ -31,3 +31,26 @@ export function createBackgroundLayer(id: string, fill: string): Layer {
|
|
|
31
31
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Estimate the rendered height (px) of a wrapped text block.
|
|
37
|
+
*
|
|
38
|
+
* Templates are pure functions with no access to real text metrics, but
|
|
39
|
+
* stacked lockups (quote + attribution, fact + explanation + source) need
|
|
40
|
+
* to place each element relative to the previous one instead of at fixed
|
|
41
|
+
* slots — fixed slots leave 200px voids for short content and collide for
|
|
42
|
+
* long content. The 0.52 average-glyph-width factor is tuned for the
|
|
43
|
+
* sans/serif faces the built-in themes use; it only needs to be right
|
|
44
|
+
* within ~20% for spacing purposes.
|
|
45
|
+
*/
|
|
46
|
+
export function estimateTextHeight(
|
|
47
|
+
text: string,
|
|
48
|
+
fontSizePx: number,
|
|
49
|
+
maxWidthPx: number,
|
|
50
|
+
lineHeight: number,
|
|
51
|
+
): number {
|
|
52
|
+
const avgCharWidth = fontSizePx * 0.52;
|
|
53
|
+
const charsPerLine = Math.max(8, Math.floor(maxWidthPx / avgCharWidth));
|
|
54
|
+
const lines = Math.max(1, Math.ceil(text.length / charsPerLine));
|
|
55
|
+
return lines * fontSizePx * lineHeight;
|
|
56
|
+
}
|
|
@@ -10,28 +10,20 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Layer } from '../../schemas/Doc.js';
|
|
12
12
|
import type { ComparisonBarInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
resolveColorScheme,
|
|
15
|
+
getThemeFont,
|
|
16
|
+
themedFontSize,
|
|
17
|
+
themedSurfaceGradient,
|
|
18
|
+
} from '../utils/themeUtils.js';
|
|
15
19
|
|
|
16
20
|
export function comparisonBar(input: ComparisonBarInput, context: TemplateContext): Layer[] {
|
|
17
21
|
const { leftLabel, leftValue, rightLabel, rightValue, unit, colorScheme = 'blue' } = input;
|
|
18
|
-
const { theme } = context;
|
|
22
|
+
const { theme, viewport } = context;
|
|
19
23
|
const colors = resolveColorScheme(context, colorScheme);
|
|
20
24
|
|
|
21
|
-
const labelFontSize =
|
|
22
|
-
const valueFontSize =
|
|
23
|
-
|
|
24
|
-
// Calculate proportional bar widths (max bar = 65% of viewport width)
|
|
25
|
-
const maxValue = Math.max(leftValue, rightValue, 1);
|
|
26
|
-
const maxBarWidth = 65;
|
|
27
|
-
const leftBarWidth = (leftValue / maxValue) * maxBarWidth;
|
|
28
|
-
const rightBarWidth = (rightValue / maxValue) * maxBarWidth;
|
|
29
|
-
|
|
30
|
-
// Bar positioning
|
|
31
|
-
const barStartX = 15;
|
|
32
|
-
const barHeight = 6; // % of viewport height
|
|
33
|
-
const topBarY = 36;
|
|
34
|
-
const bottomBarY = 58;
|
|
25
|
+
const labelFontSize = themedFontSize(28, context, false);
|
|
26
|
+
const valueFontSize = themedFontSize(48, context, true);
|
|
35
27
|
|
|
36
28
|
// Format values for display
|
|
37
29
|
const formatValue = (v: number): string => {
|
|
@@ -43,6 +35,22 @@ export function comparisonBar(input: ComparisonBarInput, context: TemplateContex
|
|
|
43
35
|
const leftDisplay = unit ? `${formatValue(leftValue)} ${unit}` : formatValue(leftValue);
|
|
44
36
|
const rightDisplay = unit ? `${formatValue(rightValue)} ${unit}` : formatValue(rightValue);
|
|
45
37
|
|
|
38
|
+
// Bar positioning
|
|
39
|
+
const barStartX = 15;
|
|
40
|
+
const barHeight = 6; // % of viewport height
|
|
41
|
+
const topBarY = 36;
|
|
42
|
+
const bottomBarY = 58;
|
|
43
|
+
|
|
44
|
+
// Calculate proportional bar widths. The widest bar must leave room
|
|
45
|
+
// for its trailing value label inside a 96% safe area — otherwise a
|
|
46
|
+
// long label ("84 clarity score") runs off the right edge of the block.
|
|
47
|
+
const maxValue = Math.max(leftValue, rightValue, 1);
|
|
48
|
+
const longestLabelPx = Math.max(leftDisplay.length, rightDisplay.length) * valueFontSize * 0.58;
|
|
49
|
+
const labelWidthPct = (longestLabelPx / viewport.width) * 100;
|
|
50
|
+
const maxBarWidth = Math.max(20, Math.min(65, 96 - barStartX - labelWidthPct - 2));
|
|
51
|
+
const leftBarWidth = (leftValue / maxValue) * maxBarWidth;
|
|
52
|
+
const rightBarWidth = (rightValue / maxValue) * maxBarWidth;
|
|
53
|
+
|
|
46
54
|
return [
|
|
47
55
|
// Background
|
|
48
56
|
{
|
|
@@ -50,7 +58,7 @@ export function comparisonBar(input: ComparisonBarInput, context: TemplateContex
|
|
|
50
58
|
id: 'bg',
|
|
51
59
|
content: {
|
|
52
60
|
shape: 'rect',
|
|
53
|
-
fill:
|
|
61
|
+
fill: themedSurfaceGradient(context, 180),
|
|
54
62
|
},
|
|
55
63
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
56
64
|
},
|
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import type { Layer } from '../../schemas/Doc.js';
|
|
18
18
|
import type { TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
19
|
-
import { scaledFontSize } from '../../schemas/BlockTemplates.js';
|
|
20
19
|
import type { StartBlockConfig } from '../../schemas/Doc.js';
|
|
21
|
-
import { getThemeFont } from '../utils/themeUtils.js';
|
|
20
|
+
import { getThemeFont, themedFontSize, themedImageTreatment } from '../utils/themeUtils.js';
|
|
22
21
|
import { mapAmbientMotion } from './accentImage.js';
|
|
23
22
|
|
|
24
23
|
/**
|
|
@@ -39,18 +38,21 @@ export interface CoverBlockInput {
|
|
|
39
38
|
heroCredit?: string;
|
|
40
39
|
/** License identifier */
|
|
41
40
|
heroLicense?: string;
|
|
41
|
+
/** Per-block override for the theme's photographic image grade. */
|
|
42
|
+
imageTreatment?: 'none' | 'mono' | 'duotone' | 'warm' | 'cool';
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Generate cover block layers from StartBlockConfig.
|
|
46
47
|
*/
|
|
47
48
|
export function coverBlock(input: CoverBlockInput, context: TemplateContext): Layer[] {
|
|
49
|
+
const treatment = themedImageTreatment(context, input.imageTreatment);
|
|
48
50
|
const { heroSrc, heroAlt, title, subtitle, ambientMotion, heroCredit, heroLicense } = input;
|
|
49
51
|
const { theme, layout } = context;
|
|
50
52
|
|
|
51
53
|
// Scale font sizes for viewport - cover titles are larger than regular title blocks
|
|
52
|
-
const titleFontSize =
|
|
53
|
-
const subtitleFontSize =
|
|
54
|
+
const titleFontSize = themedFontSize(120, context, true);
|
|
55
|
+
const subtitleFontSize = themedFontSize(40, context, false);
|
|
54
56
|
|
|
55
57
|
const layers: Layer[] = [];
|
|
56
58
|
|
|
@@ -68,6 +70,7 @@ export function coverBlock(input: CoverBlockInput, context: TemplateContext): La
|
|
|
68
70
|
fit: 'cover',
|
|
69
71
|
credit: heroCredit,
|
|
70
72
|
license: heroLicense,
|
|
73
|
+
...(treatment ? { treatment } : {}),
|
|
71
74
|
},
|
|
72
75
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
73
76
|
animation: imageAnimation,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom template factory.
|
|
3
|
+
*
|
|
4
|
+
* Turns a `CustomTemplateDefinition` (user-authored visual design with
|
|
5
|
+
* placeholder tokens) into a runtime `TemplateFunction` that can be
|
|
6
|
+
* dropped into the template registry alongside built-ins.
|
|
7
|
+
*
|
|
8
|
+
* The generated function's job is small: read the source `Block` from
|
|
9
|
+
* `context.block` and hand the definition's layers off to
|
|
10
|
+
* `resolveTokens` for placeholder substitution. The hard work (drag-
|
|
11
|
+
* to-position, token grammar, image lookup) happens in the designer
|
|
12
|
+
* UI and the resolver, not here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Layer } from '../../schemas/Doc.js';
|
|
16
|
+
import type {
|
|
17
|
+
RawLayersInput,
|
|
18
|
+
TemplateContext,
|
|
19
|
+
TemplateFunction,
|
|
20
|
+
} from '../../schemas/BlockTemplates.js';
|
|
21
|
+
import type { CustomTemplateDefinition } from '../../schemas/CustomTemplates.js';
|
|
22
|
+
import { resolveTokens } from './tokens/resolveTokens.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a template function for a user-authored custom template.
|
|
26
|
+
*
|
|
27
|
+
* The returned function is registered under `def.name` in the merged
|
|
28
|
+
* registry (see `buildRegistry` in `templates/index.ts`). At expansion
|
|
29
|
+
* time it receives the standard `TemplateBlock` + `TemplateContext`
|
|
30
|
+
* pair; the context's `block` field carries the source markdown-derived
|
|
31
|
+
* block whose data drives token resolution.
|
|
32
|
+
*
|
|
33
|
+
* When `context.block` is missing (shouldn't happen in the normal
|
|
34
|
+
* pipeline but defensively handled), the layers are returned with
|
|
35
|
+
* tokens left unresolved — better visible placeholders than a thrown
|
|
36
|
+
* error.
|
|
37
|
+
*/
|
|
38
|
+
export function makeCustomTemplateFn(
|
|
39
|
+
def: CustomTemplateDefinition,
|
|
40
|
+
): TemplateFunction<RawLayersInput> {
|
|
41
|
+
return (_input: RawLayersInput, context: TemplateContext): Layer[] => {
|
|
42
|
+
if (!context.block) return def.layers.slice();
|
|
43
|
+
return resolveTokens(def.layers, context.block);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -11,24 +11,35 @@
|
|
|
11
11
|
|
|
12
12
|
import type { Layer } from '../../schemas/Doc.js';
|
|
13
13
|
import type { DataTableInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
getThemeFont,
|
|
16
|
+
resolveColorScheme,
|
|
17
|
+
shouldUseShadow,
|
|
18
|
+
themedFontSize,
|
|
19
|
+
themedSurfaceGradient,
|
|
20
|
+
} from '../utils/themeUtils.js';
|
|
21
|
+
import { pickContrastingText, withAlpha } from '../../schemas/colorUtils.js';
|
|
16
22
|
import { createBackgroundLayer } from './captionUtils.js';
|
|
17
23
|
|
|
18
24
|
export function dataTable(input: DataTableInput, context: TemplateContext): Layer[] {
|
|
19
25
|
const { title, headers, rows, align, colorScheme } = input;
|
|
20
|
-
const { theme } = context;
|
|
26
|
+
const { theme, viewport } = context;
|
|
21
27
|
|
|
22
28
|
const colors = resolveColorScheme(context, colorScheme);
|
|
23
|
-
const titleFontSize =
|
|
24
|
-
const tableFontSize =
|
|
29
|
+
const titleFontSize = themedFontSize(48, context, true);
|
|
30
|
+
const tableFontSize = themedFontSize(28, context, false);
|
|
25
31
|
|
|
26
|
-
const layers: Layer[] = [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const layers: Layer[] = [createBackgroundLayer('bg', themedSurfaceGradient(context, 170))];
|
|
33
|
+
|
|
34
|
+
// Size the table band from its natural content height (header + rows at
|
|
35
|
+
// ~2.4× line height for cell padding) and center the title+table as one
|
|
36
|
+
// group — a full-height band left a ~200px orphan gap under the title.
|
|
37
|
+
// `rows` is required by the schema but may be missing on partially-
|
|
38
|
+
// authored blocks in live preview; treat it as empty rather than throwing.
|
|
39
|
+
const rowCount = (Array.isArray(rows) ? rows.length : 0) + 1;
|
|
40
|
+
const naturalTableHPct = Math.min(74, ((rowCount * tableFontSize * 2.4) / viewport.height) * 100);
|
|
41
|
+
const titleBandPct = title ? (titleFontSize * 2.2 * 100) / viewport.height : 0;
|
|
42
|
+
const groupTopPct = Math.max(8, (100 - titleBandPct - naturalTableHPct) / 2);
|
|
32
43
|
|
|
33
44
|
// Optional title above the table
|
|
34
45
|
if (title) {
|
|
@@ -43,15 +54,22 @@ export function dataTable(input: DataTableInput, context: TemplateContext): Laye
|
|
|
43
54
|
fontWeight: 'bold',
|
|
44
55
|
color: theme.colors.text,
|
|
45
56
|
textAlign: 'center',
|
|
46
|
-
shadow:
|
|
57
|
+
shadow: shouldUseShadow(context),
|
|
47
58
|
},
|
|
48
59
|
},
|
|
49
|
-
position: {
|
|
60
|
+
position: {
|
|
61
|
+
x: '50%',
|
|
62
|
+
y: `${groupTopPct + titleBandPct / 2}%`,
|
|
63
|
+
width: '80%',
|
|
64
|
+
anchor: 'center',
|
|
65
|
+
},
|
|
50
66
|
animation: { type: 'fadeIn', duration: 0.8 },
|
|
51
67
|
});
|
|
52
68
|
}
|
|
53
69
|
|
|
54
|
-
// Table layer
|
|
70
|
+
// Table layer. Header text color is picked for contrast against the
|
|
71
|
+
// header fill — pairing two mid-tones from the same scheme made the
|
|
72
|
+
// header row read as a smudge in most themes.
|
|
55
73
|
layers.push({
|
|
56
74
|
type: 'table',
|
|
57
75
|
id: 'table',
|
|
@@ -61,10 +79,10 @@ export function dataTable(input: DataTableInput, context: TemplateContext): Laye
|
|
|
61
79
|
align,
|
|
62
80
|
style: {
|
|
63
81
|
headerBackground: colors.accent,
|
|
64
|
-
headerColor: colors.
|
|
65
|
-
cellBackground:
|
|
82
|
+
headerColor: pickContrastingText(colors.accent),
|
|
83
|
+
cellBackground: withAlpha(theme.colors.text, 0.04),
|
|
66
84
|
cellColor: theme.colors.text,
|
|
67
|
-
borderColor:
|
|
85
|
+
borderColor: withAlpha(theme.colors.text, 0.15),
|
|
68
86
|
fontSize: tableFontSize,
|
|
69
87
|
fontFamily: getThemeFont(context, 'body'),
|
|
70
88
|
headerFontFamily: getThemeFont(context, 'title'),
|
|
@@ -73,9 +91,9 @@ export function dataTable(input: DataTableInput, context: TemplateContext): Laye
|
|
|
73
91
|
},
|
|
74
92
|
position: {
|
|
75
93
|
x: '10%',
|
|
76
|
-
y:
|
|
94
|
+
y: `${groupTopPct + titleBandPct}%`,
|
|
77
95
|
width: '80%',
|
|
78
|
-
height:
|
|
96
|
+
height: `${naturalTableHPct}%`,
|
|
79
97
|
},
|
|
80
98
|
animation: { type: 'fadeIn', duration: 1, delay: title ? 0.4 : 0 },
|
|
81
99
|
});
|
|
@@ -12,37 +12,52 @@
|
|
|
12
12
|
|
|
13
13
|
import type { Layer } from '../../schemas/Doc.js';
|
|
14
14
|
import type { DateEventInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
15
|
+
import {
|
|
16
|
+
getThemeFont,
|
|
17
|
+
shouldUseShadow,
|
|
18
|
+
themedFontSize,
|
|
19
|
+
themedSurfaceGradient,
|
|
20
|
+
themedImageTreatment,
|
|
21
|
+
} from '../utils/themeUtils.js';
|
|
22
|
+
import { oklchDarken, relativeLuminance } from '../../schemas/colorUtils.js';
|
|
17
23
|
import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Mood accents tint the hero date, not the surface — the surface always
|
|
27
|
+
* comes from the theme. (Hard-coded dark mood panels used to turn light
|
|
28
|
+
* and warm themes into a cold navy slide.)
|
|
29
|
+
*/
|
|
30
|
+
const MOOD_ACCENTS = {
|
|
31
|
+
neutral: '#63b3ed',
|
|
32
|
+
somber: '#e53e3e',
|
|
33
|
+
celebratory: '#68d391',
|
|
23
34
|
};
|
|
24
35
|
|
|
25
36
|
export function dateEvent(input: DateEventInput, context: TemplateContext): Layer[] {
|
|
26
37
|
const { date, description, footer, mood = 'neutral', accentImage } = input;
|
|
27
38
|
const { theme } = context;
|
|
28
|
-
|
|
39
|
+
// Deepen the mood accent on light surfaces so the hero date keeps contrast.
|
|
40
|
+
const moodAccent = MOOD_ACCENTS[mood];
|
|
41
|
+
const dateColor =
|
|
42
|
+
relativeLuminance(theme.colors.background) > 0.5 ? oklchDarken(moodAccent, 0.25) : moodAccent;
|
|
29
43
|
|
|
30
44
|
// Get layout adjustments if accent image is present
|
|
31
45
|
const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
|
|
32
46
|
|
|
33
47
|
// Scale font sizes — date is the hero element
|
|
34
|
-
const dateFontSize =
|
|
35
|
-
const descFontSize =
|
|
36
|
-
const footerFontSize =
|
|
48
|
+
const dateFontSize = themedFontSize(96, context, true);
|
|
49
|
+
const descFontSize = themedFontSize(30, context, false);
|
|
50
|
+
const footerFontSize = themedFontSize(26, context, false);
|
|
37
51
|
|
|
38
52
|
const layers: Layer[] = [
|
|
39
|
-
// Background —
|
|
53
|
+
// Background — theme surface gradient (diagonal to differentiate
|
|
54
|
+
// from other text templates)
|
|
40
55
|
{
|
|
41
56
|
type: 'shape',
|
|
42
57
|
id: 'bg',
|
|
43
58
|
content: {
|
|
44
59
|
shape: 'rect',
|
|
45
|
-
fill:
|
|
60
|
+
fill: themedSurfaceGradient(context, 135),
|
|
46
61
|
},
|
|
47
62
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
48
63
|
},
|
|
@@ -50,7 +65,13 @@ export function dateEvent(input: DateEventInput, context: TemplateContext): Laye
|
|
|
50
65
|
|
|
51
66
|
// Add accent image layers (behind text, after background)
|
|
52
67
|
if (accentImage) {
|
|
53
|
-
layers.push(
|
|
68
|
+
layers.push(
|
|
69
|
+
...createAccentLayers(
|
|
70
|
+
accentImage,
|
|
71
|
+
input.id,
|
|
72
|
+
themedImageTreatment(context, input.imageTreatment),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
// Date — hero element, much larger
|
|
@@ -63,8 +84,8 @@ export function dateEvent(input: DateEventInput, context: TemplateContext): Laye
|
|
|
63
84
|
fontSize: dateFontSize,
|
|
64
85
|
fontFamily: getThemeFont(context, 'title'),
|
|
65
86
|
fontWeight: 'bold',
|
|
66
|
-
color:
|
|
67
|
-
shadow:
|
|
87
|
+
color: dateColor,
|
|
88
|
+
shadow: shouldUseShadow(context),
|
|
68
89
|
},
|
|
69
90
|
},
|
|
70
91
|
position: {
|
|
@@ -84,10 +105,10 @@ export function dateEvent(input: DateEventInput, context: TemplateContext): Laye
|
|
|
84
105
|
style: {
|
|
85
106
|
fontSize: descFontSize,
|
|
86
107
|
fontFamily: getThemeFont(context, 'body'),
|
|
87
|
-
color: colors.text,
|
|
108
|
+
color: theme.colors.text,
|
|
88
109
|
textAlign: 'center',
|
|
89
|
-
lineHeight: 1.
|
|
90
|
-
shadow:
|
|
110
|
+
lineHeight: 1.6,
|
|
111
|
+
shadow: shouldUseShadow(context),
|
|
91
112
|
},
|
|
92
113
|
},
|
|
93
114
|
position: {
|
|
@@ -110,12 +131,12 @@ export function dateEvent(input: DateEventInput, context: TemplateContext): Laye
|
|
|
110
131
|
fontSize: footerFontSize,
|
|
111
132
|
fontFamily: getThemeFont(context, 'body'),
|
|
112
133
|
color: theme.colors.textMuted,
|
|
113
|
-
shadow:
|
|
134
|
+
shadow: shouldUseShadow(context),
|
|
114
135
|
},
|
|
115
136
|
},
|
|
116
137
|
position: {
|
|
117
138
|
x: accentLayout.textCenterX,
|
|
118
|
-
y: adjustY('
|
|
139
|
+
y: adjustY('72%', accentLayout),
|
|
119
140
|
anchor: 'center',
|
|
120
141
|
},
|
|
121
142
|
animation: { type: 'fadeIn', duration: 1, delay: 3 },
|