@bendyline/squisq 2.0.0 → 2.0.1
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/{chunk-QTSJOY54.js → chunk-2PWOO4ZE.js} +2 -2
- package/dist/{chunk-T3TI7GSZ.js → chunk-CY2R2VPR.js} +2 -2
- package/dist/{chunk-LYNJEOAC.js → chunk-MMPBOUK7.js} +2 -2
- package/dist/{chunk-QVANWDPH.js → chunk-TFJVXUHH.js} +3 -3
- package/dist/{chunk-6PJRB2CF.js → chunk-W4O2TX5I.js} +2 -2
- package/dist/{chunk-6PJRB2CF.js.map → chunk-W4O2TX5I.js.map} +1 -1
- package/dist/{chunk-GF3BVNLU.js → chunk-X6DUONGT.js} +74 -19
- package/dist/chunk-X6DUONGT.js.map +1 -0
- package/dist/doc/index.js +3 -3
- package/dist/index.js +6 -6
- package/dist/jsonForm/index.js +3 -3
- package/dist/narration/index.js +3 -3
- package/dist/schemas/index.js +2 -2
- package/package.json +1 -1
- package/src/__tests__/customThemesFrontmatter.test.ts +1 -0
- package/src/__tests__/markdown.test.ts +13 -4
- package/src/__tests__/markdownSanitize.test.ts +7 -5
- package/src/doc/markdownToDoc.ts +3 -1
- package/src/doc/templates/__tests__/listBlock.test.ts +64 -0
- package/src/doc/templates/listBlock.ts +88 -30
- package/src/schemas/themes/gezellig.json +1 -1
- package/dist/chunk-GF3BVNLU.js.map +0 -1
- /package/dist/{chunk-QTSJOY54.js.map → chunk-2PWOO4ZE.js.map} +0 -0
- /package/dist/{chunk-T3TI7GSZ.js.map → chunk-CY2R2VPR.js.map} +0 -0
- /package/dist/{chunk-LYNJEOAC.js.map → chunk-MMPBOUK7.js.map} +0 -0
- /package/dist/{chunk-QVANWDPH.js.map → chunk-TFJVXUHH.js.map} +0 -0
package/dist/doc/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
scoreTextSimilarity,
|
|
17
17
|
validateMarkdownDoc,
|
|
18
18
|
validateMarkdownSource
|
|
19
|
-
} from "../chunk-
|
|
19
|
+
} from "../chunk-TFJVXUHH.js";
|
|
20
20
|
import {
|
|
21
21
|
ASCII_CHAR_H,
|
|
22
22
|
ASCII_CHAR_W,
|
|
@@ -129,7 +129,7 @@ import {
|
|
|
129
129
|
wrapWithPersistentLayers,
|
|
130
130
|
writeCustomTemplatesToFrontmatter,
|
|
131
131
|
writeCustomThemesToFrontmatter
|
|
132
|
-
} from "../chunk-
|
|
132
|
+
} from "../chunk-X6DUONGT.js";
|
|
133
133
|
import {
|
|
134
134
|
PATH_SHAPE_KINDS,
|
|
135
135
|
anchorPoint,
|
|
@@ -174,7 +174,7 @@ import {
|
|
|
174
174
|
getAvailableThemes,
|
|
175
175
|
getThemeSummaries,
|
|
176
176
|
resolveTheme
|
|
177
|
-
} from "../chunk-
|
|
177
|
+
} from "../chunk-W4O2TX5I.js";
|
|
178
178
|
import {
|
|
179
179
|
VIEWPORT_PRESETS,
|
|
180
180
|
createTemplateContext,
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
reanchorSession,
|
|
24
24
|
traceWordPosAt,
|
|
25
25
|
vadStep
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-CY2R2VPR.js";
|
|
27
27
|
import "./chunk-ET53IIEP.js";
|
|
28
28
|
import {
|
|
29
29
|
DEFAULT_TRANSFORM_STYLE_ID,
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
resolveRef,
|
|
70
70
|
setByPointer,
|
|
71
71
|
toPointer
|
|
72
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-2PWOO4ZE.js";
|
|
73
73
|
import {
|
|
74
74
|
IMAGE_EDIT_ASSETS_PREFIX,
|
|
75
75
|
IMAGE_EDIT_DEFAULT_BASENAME,
|
|
@@ -122,7 +122,7 @@ import {
|
|
|
122
122
|
getSegmentAtTime,
|
|
123
123
|
parseTheme,
|
|
124
124
|
serializeTheme
|
|
125
|
-
} from "./chunk-
|
|
125
|
+
} from "./chunk-MMPBOUK7.js";
|
|
126
126
|
import {
|
|
127
127
|
calculateBearing,
|
|
128
128
|
decodeGeohash,
|
|
@@ -153,7 +153,7 @@ import {
|
|
|
153
153
|
scoreTextSimilarity,
|
|
154
154
|
validateMarkdownDoc,
|
|
155
155
|
validateMarkdownSource
|
|
156
|
-
} from "./chunk-
|
|
156
|
+
} from "./chunk-TFJVXUHH.js";
|
|
157
157
|
import {
|
|
158
158
|
ASCII_CHAR_H,
|
|
159
159
|
ASCII_CHAR_W,
|
|
@@ -274,7 +274,7 @@ import {
|
|
|
274
274
|
wrapWithPersistentLayers,
|
|
275
275
|
writeCustomTemplatesToFrontmatter,
|
|
276
276
|
writeCustomThemesToFrontmatter
|
|
277
|
-
} from "./chunk-
|
|
277
|
+
} from "./chunk-X6DUONGT.js";
|
|
278
278
|
import {
|
|
279
279
|
PATH_SHAPE_KINDS,
|
|
280
280
|
anchorPoint,
|
|
@@ -352,7 +352,7 @@ import {
|
|
|
352
352
|
resolveTheme,
|
|
353
353
|
validateTheme,
|
|
354
354
|
withAlpha
|
|
355
|
-
} from "./chunk-
|
|
355
|
+
} from "./chunk-W4O2TX5I.js";
|
|
356
356
|
import {
|
|
357
357
|
DEFAULT_DOC_FONT,
|
|
358
358
|
DEFAULT_TITLE_FONT,
|
package/dist/jsonForm/index.js
CHANGED
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
resolveRef,
|
|
15
15
|
setByPointer,
|
|
16
16
|
toPointer
|
|
17
|
-
} from "../chunk-
|
|
18
|
-
import "../chunk-
|
|
17
|
+
} from "../chunk-2PWOO4ZE.js";
|
|
18
|
+
import "../chunk-MMPBOUK7.js";
|
|
19
19
|
import "../chunk-FVJUUTEM.js";
|
|
20
|
-
import "../chunk-
|
|
20
|
+
import "../chunk-W4O2TX5I.js";
|
|
21
21
|
import "../chunk-5J4HKRJF.js";
|
|
22
22
|
import "../chunk-QE7TWGVT.js";
|
|
23
23
|
export {
|
package/dist/narration/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
reanchorSession,
|
|
24
24
|
traceWordPosAt,
|
|
25
25
|
vadStep
|
|
26
|
-
} from "../chunk-
|
|
26
|
+
} from "../chunk-CY2R2VPR.js";
|
|
27
27
|
import {
|
|
28
28
|
buildNarrationScript,
|
|
29
29
|
buildNarrationTimingJson,
|
|
@@ -33,10 +33,10 @@ import {
|
|
|
33
33
|
wordIndexAtChar,
|
|
34
34
|
wordIndexAtTime,
|
|
35
35
|
wordPosAtExpectedSyllables
|
|
36
|
-
} from "../chunk-
|
|
36
|
+
} from "../chunk-X6DUONGT.js";
|
|
37
37
|
import "../chunk-TUWMK62B.js";
|
|
38
38
|
import "../chunk-5UYI3RJB.js";
|
|
39
|
-
import "../chunk-
|
|
39
|
+
import "../chunk-W4O2TX5I.js";
|
|
40
40
|
import "../chunk-5J4HKRJF.js";
|
|
41
41
|
import "../chunk-R2YZTCLO.js";
|
|
42
42
|
import "../chunk-AMIR72JP.js";
|
package/dist/schemas/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
getSegmentAtTime,
|
|
10
10
|
parseTheme,
|
|
11
11
|
serializeTheme
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-MMPBOUK7.js";
|
|
13
13
|
import {
|
|
14
14
|
getDocPlaybackDuration,
|
|
15
15
|
resolveMediaSchedule
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
resolveTheme,
|
|
50
50
|
validateTheme,
|
|
51
51
|
withAlpha
|
|
52
|
-
} from "../chunk-
|
|
52
|
+
} from "../chunk-W4O2TX5I.js";
|
|
53
53
|
import {
|
|
54
54
|
DEFAULT_DOC_FONT,
|
|
55
55
|
DEFAULT_TITLE_FONT,
|
package/package.json
CHANGED
|
@@ -115,6 +115,7 @@ squisq-custom-themes: ${encoded}
|
|
|
115
115
|
A small editor for big ideas.
|
|
116
116
|
`;
|
|
117
117
|
const doc = markdownToDoc(parseMarkdown(sourceMd));
|
|
118
|
+
expect(doc.themeId).toBe('my-brand');
|
|
118
119
|
expect(doc.customThemes).toHaveLength(1);
|
|
119
120
|
expect(doc.customThemes![0]).toEqual(brand);
|
|
120
121
|
|
|
@@ -155,14 +155,14 @@ describe('parseMarkdown / stringifyMarkdown', () => {
|
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
it('parses mentions from @[Name](scheme:id)', () => {
|
|
158
|
-
const doc = parse('Hey @[Leo](
|
|
158
|
+
const doc = parse('Hey @[Leo](person:leo), take a look.');
|
|
159
159
|
const p = doc.children[0] as MarkdownParagraph;
|
|
160
160
|
const text0 = p.children[0] as MarkdownText;
|
|
161
161
|
expect(text0).toEqual({ type: 'text', value: 'Hey ' });
|
|
162
162
|
const mention = p.children[1] as MarkdownMention;
|
|
163
163
|
expect(mention).toEqual({
|
|
164
164
|
type: 'mention',
|
|
165
|
-
targetKind: '
|
|
165
|
+
targetKind: 'person',
|
|
166
166
|
targetId: 'leo',
|
|
167
167
|
displayName: 'Leo',
|
|
168
168
|
});
|
|
@@ -176,7 +176,7 @@ describe('parseMarkdown / stringifyMarkdown', () => {
|
|
|
176
176
|
// re-parsing `\:` produces the unescaped url, so the round-trip
|
|
177
177
|
// preserves the mention semantically even if the raw text changes
|
|
178
178
|
// on the first pass.
|
|
179
|
-
const input = 'Hey @[Leo](
|
|
179
|
+
const input = 'Hey @[Leo](person:leo), ping @[Tess](person:tess) too.\n';
|
|
180
180
|
const once = stringifyMarkdown(parseMarkdown(input));
|
|
181
181
|
const twice = stringifyMarkdown(parseMarkdown(once));
|
|
182
182
|
expect(twice).toBe(once);
|
|
@@ -184,7 +184,7 @@ describe('parseMarkdown / stringifyMarkdown', () => {
|
|
|
184
184
|
const doc = parseMarkdown(once) as MarkdownDocument;
|
|
185
185
|
const p = doc.children[0] as MarkdownParagraph;
|
|
186
186
|
const m1 = p.children.find((c) => c.type === 'mention') as MarkdownMention;
|
|
187
|
-
expect(m1.targetKind).toBe('
|
|
187
|
+
expect(m1.targetKind).toBe('person');
|
|
188
188
|
expect(m1.targetId).toBe('leo');
|
|
189
189
|
expect(m1.displayName).toBe('Leo');
|
|
190
190
|
});
|
|
@@ -944,6 +944,15 @@ describe('multi-line frontmatter round-trips', () => {
|
|
|
944
944
|
path: 'C:\\temp\\"quoted"',
|
|
945
945
|
});
|
|
946
946
|
});
|
|
947
|
+
|
|
948
|
+
it('writes boolean values as unquoted YAML booleans', () => {
|
|
949
|
+
const out = setFrontmatterValues('# Body\n', { enabled: true, disabled: false });
|
|
950
|
+
expect(out).toContain('enabled: true');
|
|
951
|
+
expect(out).toContain('disabled: false');
|
|
952
|
+
expect(out).not.toContain('enabled: "true"');
|
|
953
|
+
expect(out).not.toContain('disabled: "false"');
|
|
954
|
+
expect(parseMarkdown(out).frontmatter).toEqual({ enabled: true, disabled: false });
|
|
955
|
+
});
|
|
947
956
|
});
|
|
948
957
|
|
|
949
958
|
describe('frontmatter in parseMarkdown', () => {
|
|
@@ -18,17 +18,19 @@ describe('markdown HTML sanitization', () => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
it('extraLinkSchemes allows host-app schemes but never executable ones', () => {
|
|
21
|
-
expect(sanitizeUrl('
|
|
22
|
-
expect(
|
|
23
|
-
'
|
|
24
|
-
);
|
|
21
|
+
expect(sanitizeUrl('workspace-nav:src%2Fa.ts', 'link')).toBeNull();
|
|
22
|
+
expect(
|
|
23
|
+
sanitizeUrl('workspace-nav:src%2Fa.ts', 'link', { extraLinkSchemes: ['workspace-nav'] }),
|
|
24
|
+
).toBe('workspace-nav:src%2Fa.ts');
|
|
25
25
|
expect(
|
|
26
26
|
sanitizeUrl('javascript:alert(1)', 'link', { extraLinkSchemes: ['javascript'] }),
|
|
27
27
|
).toBeNull();
|
|
28
28
|
expect(sanitizeUrl('data:text/html,x', 'link', { extraLinkSchemes: ['data'] })).toBeNull();
|
|
29
29
|
// media URLs ignore the option entirely
|
|
30
30
|
expect(
|
|
31
|
-
sanitizeUrl('
|
|
31
|
+
sanitizeUrl('workspace-nav:src%2Fa.ts', 'media', {
|
|
32
|
+
extraLinkSchemes: ['workspace-nav'],
|
|
33
|
+
}),
|
|
32
34
|
).toBeNull();
|
|
33
35
|
});
|
|
34
36
|
|
package/src/doc/markdownToDoc.ts
CHANGED
|
@@ -40,7 +40,7 @@ import type {
|
|
|
40
40
|
MarkdownNode,
|
|
41
41
|
HtmlNode,
|
|
42
42
|
} from '../markdown/types.js';
|
|
43
|
-
import { extractPlainText } from '../markdown/utils.js';
|
|
43
|
+
import { extractPlainText, readFrontmatterThemeId } from '../markdown/utils.js';
|
|
44
44
|
import { coerceAnnotationValues, type CoercedBlockMeta } from '../markdown/annotationCoercion.js';
|
|
45
45
|
import { estimateReadingTime } from '../timing/readingTime.js';
|
|
46
46
|
import { resolveTemplateName, isContainerTemplate } from './templates/index.js';
|
|
@@ -600,6 +600,7 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
|
|
|
600
600
|
|
|
601
601
|
const customTemplates = readCustomTemplatesFromFrontmatter(markdownDoc.frontmatter);
|
|
602
602
|
const customThemes = readCustomThemesFromFrontmatter(markdownDoc.frontmatter);
|
|
603
|
+
const themeId = readFrontmatterThemeId(markdownDoc.frontmatter);
|
|
603
604
|
const doc: Doc = {
|
|
604
605
|
articleId,
|
|
605
606
|
duration: currentTime,
|
|
@@ -609,6 +610,7 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
|
|
|
609
610
|
},
|
|
610
611
|
...(captions ? { captions } : {}),
|
|
611
612
|
...(markdownDoc.frontmatter ? { frontmatter: markdownDoc.frontmatter } : {}),
|
|
613
|
+
...(themeId ? { themeId } : {}),
|
|
612
614
|
...(customTemplates ? { customTemplates } : {}),
|
|
613
615
|
...(customThemes ? { customThemes } : {}),
|
|
614
616
|
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createTemplateContext } from '../../../schemas/BlockTemplates.js';
|
|
3
|
+
import type { TextLayer } from '../../../schemas/Doc.js';
|
|
4
|
+
import { DEFAULT_THEME } from '../../../schemas/themeLibrary.js';
|
|
5
|
+
import { VIEWPORT_PRESETS } from '../../../schemas/Viewport.js';
|
|
6
|
+
import { listBlock } from '../listBlock.js';
|
|
7
|
+
|
|
8
|
+
const ITEMS = [
|
|
9
|
+
'We launched the scripting platform for Minecraft, which was essential for launching new categories of content for Minecraft (add-ons)',
|
|
10
|
+
"I spec'ed and largely built the Minecraft Creator Tools suite, including a website for beginning to advanced creators and set of NPM commands and tools.",
|
|
11
|
+
'I manage and craft the Minecraft samples and documentation suite',
|
|
12
|
+
'I produce and create the Minecraft Creator Channel on YouTube',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
describe('listBlock', () => {
|
|
16
|
+
it('reserves vertical space for wrapped list items', () => {
|
|
17
|
+
const context = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
18
|
+
const layers = listBlock(
|
|
19
|
+
{
|
|
20
|
+
template: 'list',
|
|
21
|
+
id: 'minecraft-platform',
|
|
22
|
+
duration: 10,
|
|
23
|
+
audioSegment: 0,
|
|
24
|
+
title: 'Minecraft Platform',
|
|
25
|
+
items: ITEMS,
|
|
26
|
+
},
|
|
27
|
+
context,
|
|
28
|
+
);
|
|
29
|
+
const itemLayers = layers.filter(
|
|
30
|
+
(layer): layer is TextLayer => layer.type === 'text' && /^item-\d+$/.test(layer.id),
|
|
31
|
+
);
|
|
32
|
+
const markerLayers = layers.filter(
|
|
33
|
+
(layer): layer is TextLayer => layer.type === 'text' && /^item-\d+-marker$/.test(layer.id),
|
|
34
|
+
);
|
|
35
|
+
const yPositions = itemLayers.map((layer) => parseFloat(String(layer.position.y)));
|
|
36
|
+
|
|
37
|
+
expect(itemLayers).toHaveLength(4);
|
|
38
|
+
expect(markerLayers).toHaveLength(4);
|
|
39
|
+
expect(yPositions[0]).toBe(34);
|
|
40
|
+
|
|
41
|
+
// The marker has its own right-aligned column, while all body lines are
|
|
42
|
+
// rendered by one layer beginning after that column (a hanging indent).
|
|
43
|
+
expect(markerLayers[0]!.content.text).toBe('1.');
|
|
44
|
+
expect(markerLayers[0]!.content.style.textAlign).toBe('right');
|
|
45
|
+
expect(itemLayers[0]!.content.text).toBe(ITEMS[0]);
|
|
46
|
+
const markerRightPx =
|
|
47
|
+
(parseFloat(String(markerLayers[0]!.position.x)) / 100) * context.viewport.width +
|
|
48
|
+
Number(markerLayers[0]!.position.width);
|
|
49
|
+
const bodyLeftPx =
|
|
50
|
+
(parseFloat(String(itemLayers[0]!.position.x)) / 100) * context.viewport.width;
|
|
51
|
+
expect(bodyLeftPx).toBeGreaterThan(markerRightPx);
|
|
52
|
+
|
|
53
|
+
// The first two entries wrap to two lines. Their following baselines
|
|
54
|
+
// therefore advance by two line-heights plus the inter-item gap.
|
|
55
|
+
const fontSize = itemLayers[0]!.content.style.fontSize;
|
|
56
|
+
const twoLineAdvance = ((2 * fontSize * 1.2 + 18) / context.viewport.height) * 100;
|
|
57
|
+
expect(yPositions[1]! - yPositions[0]!).toBeCloseTo(twoLineAdvance);
|
|
58
|
+
expect(yPositions[2]! - yPositions[1]!).toBeCloseTo(twoLineAdvance);
|
|
59
|
+
|
|
60
|
+
// The third entry fits on one line, so the final advance is compact.
|
|
61
|
+
const oneLineAdvance = ((fontSize * 1.2 + 18) / context.viewport.height) * 100;
|
|
62
|
+
expect(yPositions[3]! - yPositions[2]!).toBeCloseTo(oneLineAdvance);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -19,6 +19,43 @@ import {
|
|
|
19
19
|
} from '../utils/themeUtils.js';
|
|
20
20
|
import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
|
|
21
21
|
|
|
22
|
+
const LIST_ITEM_LINE_HEIGHT = 1.2;
|
|
23
|
+
const LIST_ITEM_GAP_PX = 18;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Mirror the renderer's character-based wrapping closely enough to reserve
|
|
27
|
+
* vertical space for each independently positioned text layer.
|
|
28
|
+
*/
|
|
29
|
+
function estimateWrappedLineCount(text: string, fontSize: number, maxWidth: number): number {
|
|
30
|
+
if (!text.trim()) return 1;
|
|
31
|
+
|
|
32
|
+
const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
|
|
33
|
+
if (charsPerLine <= 0) return 1;
|
|
34
|
+
|
|
35
|
+
let lineCount = 0;
|
|
36
|
+
let currentLineLength = 0;
|
|
37
|
+
|
|
38
|
+
for (const word of text.split(/\s+/)) {
|
|
39
|
+
const testLineLength = currentLineLength ? currentLineLength + 1 + word.length : word.length;
|
|
40
|
+
|
|
41
|
+
if (testLineLength <= charsPerLine) {
|
|
42
|
+
currentLineLength = testLineLength;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (currentLineLength) lineCount += 1;
|
|
47
|
+
|
|
48
|
+
let remainingLength = word.length;
|
|
49
|
+
while (remainingLength > charsPerLine) {
|
|
50
|
+
lineCount += 1;
|
|
51
|
+
remainingLength -= charsPerLine;
|
|
52
|
+
}
|
|
53
|
+
currentLineLength = remainingLength;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Math.max(1, lineCount + (currentLineLength ? 1 : 0));
|
|
57
|
+
}
|
|
58
|
+
|
|
22
59
|
export function listBlock(input: ListBlockInput, context: TemplateContext): Layer[] {
|
|
23
60
|
const { title, accentImage } = input;
|
|
24
61
|
// `items` is required by the schema, but malformed / partially-authored
|
|
@@ -66,8 +103,9 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
|
|
|
66
103
|
// defensive against future edits to the accent-layout types).
|
|
67
104
|
const centerX = parseFloat(accentLayout.textCenterX);
|
|
68
105
|
const widthPct = parseFloat(accentLayout.textWidth);
|
|
69
|
-
const
|
|
70
|
-
Number.isFinite(centerX) && Number.isFinite(widthPct) ?
|
|
106
|
+
const leftPct =
|
|
107
|
+
Number.isFinite(centerX) && Number.isFinite(widthPct) ? centerX - widthPct / 2 : 8;
|
|
108
|
+
const leftX = `${leftPct}%`;
|
|
71
109
|
|
|
72
110
|
// Title if provided
|
|
73
111
|
const startY = title ? 34 : 26;
|
|
@@ -95,28 +133,49 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
|
|
|
95
133
|
});
|
|
96
134
|
}
|
|
97
135
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const LIST_ITEM_BASE_PX = 34;
|
|
110
|
-
const LIST_ITEM_LINE_HEIGHT = 1.2;
|
|
111
|
-
const LIST_ITEM_GAP_PX = 18;
|
|
112
|
-
const DESIGN_HEIGHT_PX = 1080;
|
|
113
|
-
const spacing =
|
|
114
|
-
((LIST_ITEM_BASE_PX * LIST_ITEM_LINE_HEIGHT + LIST_ITEM_GAP_PX) / DESIGN_HEIGHT_PX) * 100;
|
|
136
|
+
// Render the number and body in separate columns. The body is therefore a
|
|
137
|
+
// single text layer whose wrapped lines all share the same x coordinate,
|
|
138
|
+
// producing a hanging indent instead of wrapping underneath the number.
|
|
139
|
+
const textWidthPx = (Number.isFinite(widthPct) ? widthPct / 100 : 0.85) * context.viewport.width;
|
|
140
|
+
const markerText = `${items.length}.`;
|
|
141
|
+
const markerWidthPx = Math.max(itemFontSize, markerText.length * itemFontSize * 0.5);
|
|
142
|
+
const markerGapPx = itemFontSize * 0.35;
|
|
143
|
+
const bodyIndentPx = markerWidthPx + markerGapPx;
|
|
144
|
+
const bodyLeftX = `${leftPct + (bodyIndentPx / context.viewport.width) * 100}%`;
|
|
145
|
+
const bodyWidthPx = Math.max(itemFontSize, textWidthPx - bodyIndentPx);
|
|
146
|
+
let itemY = startY;
|
|
115
147
|
|
|
116
148
|
// List items with staggered animation
|
|
117
149
|
for (let i = 0; i < items.length; i++) {
|
|
118
|
-
const
|
|
119
|
-
const
|
|
150
|
+
const itemText = items[i]!;
|
|
151
|
+
const lineCount = estimateWrappedLineCount(itemText, itemFontSize, bodyWidthPx);
|
|
152
|
+
const animation = themedEntrance(context, 'text', {
|
|
153
|
+
type: 'fadeIn',
|
|
154
|
+
duration: 0.8,
|
|
155
|
+
delay: 0.3 + 0.3 * i,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
layers.push({
|
|
159
|
+
type: 'text',
|
|
160
|
+
id: `item-${i}-marker`,
|
|
161
|
+
content: {
|
|
162
|
+
text: `${i + 1}.`,
|
|
163
|
+
style: {
|
|
164
|
+
fontSize: itemFontSize,
|
|
165
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
166
|
+
color: theme.colors.text,
|
|
167
|
+
textAlign: 'right',
|
|
168
|
+
lineHeight: LIST_ITEM_LINE_HEIGHT,
|
|
169
|
+
shadow: shouldUseShadow(context),
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
position: {
|
|
173
|
+
x: leftX,
|
|
174
|
+
y: adjustY(`${itemY}%`, accentLayout),
|
|
175
|
+
width: markerWidthPx,
|
|
176
|
+
},
|
|
177
|
+
animation,
|
|
178
|
+
});
|
|
120
179
|
|
|
121
180
|
layers.push({
|
|
122
181
|
type: 'text',
|
|
@@ -128,21 +187,20 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
|
|
|
128
187
|
fontFamily: getThemeFont(context, 'body'),
|
|
129
188
|
color: theme.colors.text,
|
|
130
189
|
textAlign: 'left',
|
|
131
|
-
lineHeight:
|
|
190
|
+
lineHeight: LIST_ITEM_LINE_HEIGHT,
|
|
132
191
|
shadow: shouldUseShadow(context),
|
|
133
192
|
},
|
|
134
193
|
},
|
|
135
194
|
position: {
|
|
136
|
-
x:
|
|
137
|
-
y: adjustY(`${
|
|
138
|
-
width:
|
|
195
|
+
x: bodyLeftX,
|
|
196
|
+
y: adjustY(`${itemY}%`, accentLayout),
|
|
197
|
+
width: bodyWidthPx,
|
|
139
198
|
},
|
|
140
|
-
animation
|
|
141
|
-
type: 'fadeIn',
|
|
142
|
-
duration: 0.8,
|
|
143
|
-
delay: 0.3 + 0.3 * i,
|
|
144
|
-
}),
|
|
199
|
+
animation,
|
|
145
200
|
});
|
|
201
|
+
|
|
202
|
+
const itemHeightPx = lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT;
|
|
203
|
+
itemY += ((itemHeightPx + LIST_ITEM_GAP_PX) / context.viewport.height) * 100;
|
|
146
204
|
}
|
|
147
205
|
|
|
148
206
|
return layers;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": "1",
|
|
3
3
|
"id": "gezellig",
|
|
4
4
|
"name": "Gezellig",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Cozy, warm-hued palette with a subtle orange-tinted dark background, cream text, and sage and terracotta accents.",
|
|
6
6
|
"colors": {
|
|
7
7
|
"primary": "#b0724c",
|
|
8
8
|
"secondary": "#667f62",
|