slidev-theme-practicum 0.1.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/README.md +270 -0
- package/components/DebugGrid.vue +50 -0
- package/components/DecorLibrary.vue +1117 -0
- package/components/Header.vue +124 -0
- package/components/Image.vue +82 -0
- package/components/ImageRenderer.vue +126 -0
- package/components/Logo.vue +142 -0
- package/components/Person.vue +89 -0
- package/components/Slide.vue +324 -0
- package/components/Slot.vue +419 -0
- package/components/Text.vue +216 -0
- package/components/Timeline.vue +202 -0
- package/composables/decor-catalog.mjs +339 -0
- package/composables/decor-config.mjs +488 -0
- package/composables/decor-file-store.ts +192 -0
- package/composables/decor-http-store.ts +25 -0
- package/composables/decor-store.ts +39 -0
- package/composables/decor-tuning-overrides.mjs +2 -0
- package/composables/decor-workbench.ts +799 -0
- package/composables/grid-placement.ts +396 -0
- package/composables/image-config.mjs +484 -0
- package/composables/layout-authoring.ts +372 -0
- package/composables/layout-markdown.mjs +56 -0
- package/composables/layout-recipes.ts +347 -0
- package/composables/layout-registry.ts +47 -0
- package/composables/layout-shorthands.ts +891 -0
- package/composables/layout-vnode.ts +43 -0
- package/composables/logo-mark.mjs +5 -0
- package/composables/slide-layout.ts +43 -0
- package/composables/slot-placement.ts +195 -0
- package/composables/slot-rules.ts +147 -0
- package/composables/text-fit-runtime.ts +577 -0
- package/composables/text-priority-fit.mjs +166 -0
- package/composables/text-typography.mjs +138 -0
- package/composables/theme-foundation.ts +192 -0
- package/composables/theme-media.mjs +202 -0
- package/composables/use-theme-config.ts +71 -0
- package/decor-library.md +10 -0
- package/env.d.ts +6 -0
- package/example.md +786 -0
- package/layouts/collection.vue +5 -0
- package/layouts/cover.vue +5 -0
- package/layouts/default.vue +5 -0
- package/layouts/explainer.vue +5 -0
- package/layouts/message.vue +5 -0
- package/layouts/none.vue +5 -0
- package/layouts/section.vue +5 -0
- package/layouts/two-cols.vue +11 -0
- package/package.json +84 -0
- package/public/decor/decor-1.svg +83 -0
- package/public/decor/decor-10.svg +40 -0
- package/public/decor/decor-11.svg +162 -0
- package/public/decor/decor-12.svg +83 -0
- package/public/decor/decor-13.svg +5 -0
- package/public/decor/decor-14.svg +7 -0
- package/public/decor/decor-15.svg +10 -0
- package/public/decor/decor-16.svg +9 -0
- package/public/decor/decor-17.png +0 -0
- package/public/decor/decor-18.png +0 -0
- package/public/decor/decor-19.svg +5 -0
- package/public/decor/decor-2.png +0 -0
- package/public/decor/decor-3.svg +9 -0
- package/public/decor/decor-4.svg +31 -0
- package/public/decor/decor-5.png +0 -0
- package/public/decor/decor-6.png +0 -0
- package/public/decor/decor-7.png +0 -0
- package/public/decor/decor-8.svg +21 -0
- package/public/decor/decor-9.svg +38 -0
- package/public/favicon.svg +11 -0
- package/public/photos/photo-1.png +0 -0
- package/public/photos/photo-10.png +0 -0
- package/public/photos/photo-2.png +0 -0
- package/public/photos/photo-3.png +0 -0
- package/public/photos/photo-4.png +0 -0
- package/public/photos/photo-5.png +0 -0
- package/public/photos/photo-6.png +0 -0
- package/public/photos/photo-7.png +0 -0
- package/public/photos/photo-8.png +0 -0
- package/public/photos/photo-9.png +0 -0
- package/setup/main.ts +3 -0
- package/setup/shiki.ts +10 -0
- package/setup/vite-plugins.ts +106 -0
- package/slidev-theme-practicum.png +0 -0
- package/slidev-theme-practicum.svg +102 -0
- package/styles/example.css +0 -0
- package/styles/index.css +192 -0
- package/styles/vars.css +119 -0
- package/types/slidev-client.d.ts +15 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
import { Text as NativeText, cloneVNode, h, isVNode, withCtx, type Component, type VNode } from 'vue'
|
|
2
|
+
import { extractMarkdownHeading, extractMarkdownQuote } from './layout-markdown.mjs'
|
|
3
|
+
import type { ThemeLayoutRole } from './layout-registry'
|
|
4
|
+
import { isBlankTextVNode, isNamedComponentVNode, readVNodeProp, readVNodeTypeName } from './layout-vnode'
|
|
5
|
+
import type { ThemeTextSizeInput } from './text-fit-runtime'
|
|
6
|
+
|
|
7
|
+
export type LayoutShorthandComponents = {
|
|
8
|
+
Image: Component
|
|
9
|
+
Person: Component
|
|
10
|
+
Slot: Component
|
|
11
|
+
Text: Component
|
|
12
|
+
Timeline: Component
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type LayoutShorthandContext = {
|
|
16
|
+
variant: string
|
|
17
|
+
readFrontmatterValue: (key: string) => unknown
|
|
18
|
+
readFrontmatterText: (key: string) => string
|
|
19
|
+
readFrontmatterArray: (key: string) => unknown[]
|
|
20
|
+
readFrontmatterObject: (key: string) => Record<string, unknown> | null
|
|
21
|
+
frontmatterDecor: Record<string, unknown> | null
|
|
22
|
+
agendaDecor: Record<string, unknown> | null
|
|
23
|
+
components: LayoutShorthandComponents
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type LayoutShorthandBuilder = (children: VNode[], context: LayoutShorthandContext) => VNode[]
|
|
27
|
+
|
|
28
|
+
function failMarkdownContract(label: string, message: string): never {
|
|
29
|
+
throw new Error(`[Slide] ${label} ${message}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isHeadingNode(node: VNode) {
|
|
33
|
+
return typeof node.type === 'string' && /^h[1-6]$/.test(node.type)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isTextNode(node: VNode) {
|
|
37
|
+
return node.type === NativeText
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isListNode(node: VNode) {
|
|
41
|
+
return node.type === 'ul' || node.type === 'ol'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isParagraphNode(node: VNode) {
|
|
45
|
+
return node.type === 'p'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isBlockquoteNode(node: VNode) {
|
|
49
|
+
return node.type === 'blockquote'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readTextNode(node: VNode) {
|
|
53
|
+
return typeof node.children === 'string' ? node.children : ''
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeInlineText(value: string) {
|
|
57
|
+
return value.replace(/\s+/g, ' ').trim()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readInlineText(node: VNode): string {
|
|
61
|
+
if (node.type === NativeText)
|
|
62
|
+
return normalizeInlineText(readTextNode(node))
|
|
63
|
+
|
|
64
|
+
if (typeof node.children === 'string')
|
|
65
|
+
return normalizeInlineText(node.children)
|
|
66
|
+
|
|
67
|
+
if (Array.isArray(node.children))
|
|
68
|
+
return normalizeInlineText(node.children.filter(isVNode).map(readInlineText).join(' '))
|
|
69
|
+
|
|
70
|
+
return ''
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isBlankTextNode(node: VNode) {
|
|
74
|
+
return isBlankTextVNode(node)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ensureOnlySupportedChildren(label: string, children: VNode[], allow: (node: VNode) => boolean) {
|
|
78
|
+
const unsupportedNodes = children.filter(node => !allow(node))
|
|
79
|
+
if (!unsupportedNodes.length)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
const unsupportedLabels = unsupportedNodes.map(readVNodeTypeName).join(', ')
|
|
83
|
+
failMarkdownContract(label, `поддерживает только каноническую markdown-структуру. Неподдерживаемые children: ${unsupportedLabels}.`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readHeadingText(children: VNode[], label: string) {
|
|
87
|
+
const heading = children.find(isHeadingNode)
|
|
88
|
+
if (heading)
|
|
89
|
+
return { node: heading, text: readInlineText(heading) }
|
|
90
|
+
|
|
91
|
+
const headingTextNode = children.find((node) => {
|
|
92
|
+
if (!isTextNode(node))
|
|
93
|
+
return false
|
|
94
|
+
|
|
95
|
+
return extractMarkdownHeading(readTextNode(node)) != null
|
|
96
|
+
})
|
|
97
|
+
const parsedHeading = headingTextNode ? extractMarkdownHeading(readTextNode(headingTextNode)) : null
|
|
98
|
+
|
|
99
|
+
if (parsedHeading)
|
|
100
|
+
return { node: headingTextNode, text: parsedHeading.text }
|
|
101
|
+
|
|
102
|
+
failMarkdownContract(label, 'ожидает один markdown heading внутри default slot.')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readParagraphText(children: VNode[]) {
|
|
106
|
+
const paragraph = children.find(isParagraphNode)
|
|
107
|
+
if (paragraph)
|
|
108
|
+
return { node: paragraph, text: readInlineText(paragraph) }
|
|
109
|
+
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hText(
|
|
114
|
+
context: LayoutShorthandContext,
|
|
115
|
+
as: string,
|
|
116
|
+
size: ThemeTextSizeInput,
|
|
117
|
+
content: string,
|
|
118
|
+
options: {
|
|
119
|
+
muted?: boolean
|
|
120
|
+
maxSize?: boolean
|
|
121
|
+
priority?: 1 | 2 | 3
|
|
122
|
+
align?: 'start' | 'middle' | 'end'
|
|
123
|
+
className?: string
|
|
124
|
+
typography?: 'compact'
|
|
125
|
+
} = {},
|
|
126
|
+
) {
|
|
127
|
+
const textProps: {
|
|
128
|
+
as: string
|
|
129
|
+
size: ThemeTextSizeInput
|
|
130
|
+
muted?: boolean
|
|
131
|
+
maxSize?: boolean
|
|
132
|
+
priority?: 1 | 2 | 3
|
|
133
|
+
align?: 'start' | 'middle' | 'end'
|
|
134
|
+
class?: string
|
|
135
|
+
} & {
|
|
136
|
+
'data-typography'?: 'compact'
|
|
137
|
+
} = {
|
|
138
|
+
as,
|
|
139
|
+
size,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (options.muted !== undefined)
|
|
143
|
+
textProps.muted = options.muted
|
|
144
|
+
if (options.maxSize !== undefined)
|
|
145
|
+
textProps.maxSize = options.maxSize
|
|
146
|
+
if (options.priority !== undefined)
|
|
147
|
+
textProps.priority = options.priority
|
|
148
|
+
if (options.align !== undefined)
|
|
149
|
+
textProps.align = options.align
|
|
150
|
+
if (options.className)
|
|
151
|
+
textProps.class = options.className
|
|
152
|
+
if (options.typography)
|
|
153
|
+
textProps['data-typography'] = options.typography
|
|
154
|
+
|
|
155
|
+
return h(context.components.Text, {
|
|
156
|
+
...textProps,
|
|
157
|
+
}, {
|
|
158
|
+
default: withCtx(() => [content]),
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function hSlot(
|
|
163
|
+
context: LayoutShorthandContext,
|
|
164
|
+
role: ThemeLayoutRole,
|
|
165
|
+
label: string,
|
|
166
|
+
slotProps: Record<string, unknown>,
|
|
167
|
+
children: VNode[],
|
|
168
|
+
) {
|
|
169
|
+
return h(context.components.Slot, {
|
|
170
|
+
role,
|
|
171
|
+
label,
|
|
172
|
+
...slotProps,
|
|
173
|
+
}, {
|
|
174
|
+
default: withCtx(() => children),
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readRecordText(record: Record<string, unknown>, keys: readonly string[]) {
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
const value = record[key]
|
|
181
|
+
|
|
182
|
+
if (typeof value === 'string' && value.trim())
|
|
183
|
+
return value.trim()
|
|
184
|
+
|
|
185
|
+
if (typeof value === 'number')
|
|
186
|
+
return String(value)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return ''
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function readRecordInput(record: Record<string, unknown>, keys: readonly string[]) {
|
|
193
|
+
for (const key of keys) {
|
|
194
|
+
const value = record[key]
|
|
195
|
+
|
|
196
|
+
if (typeof value === 'string' && value.trim())
|
|
197
|
+
return value.trim()
|
|
198
|
+
|
|
199
|
+
if (typeof value === 'number')
|
|
200
|
+
return value
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return undefined
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function readListItems(list: VNode, label: string) {
|
|
207
|
+
if (!Array.isArray(list.children))
|
|
208
|
+
failMarkdownContract(label, 'ожидает list items с текстом.')
|
|
209
|
+
|
|
210
|
+
return list.children
|
|
211
|
+
.filter(isVNode)
|
|
212
|
+
.filter(node => node.type === 'li')
|
|
213
|
+
.map(readInlineText)
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
type NestedListItem = { value: string, label: string }
|
|
218
|
+
|
|
219
|
+
function readNestedListItems(list: VNode, label: string): NestedListItem[] {
|
|
220
|
+
if (!Array.isArray(list.children))
|
|
221
|
+
failMarkdownContract(label, 'ожидает list items с текстом.')
|
|
222
|
+
|
|
223
|
+
return list.children
|
|
224
|
+
.filter(isVNode)
|
|
225
|
+
.filter(node => node.type === 'li')
|
|
226
|
+
.map((li) => {
|
|
227
|
+
if (!Array.isArray(li.children)) {
|
|
228
|
+
const text = typeof li.children === 'string' ? normalizeInlineText(li.children) : ''
|
|
229
|
+
return { value: text, label: '' }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const childNodes = li.children.filter(isVNode)
|
|
233
|
+
const nestedList = childNodes.find(node => node.type === 'ul' || node.type === 'ol')
|
|
234
|
+
const valueNodes = childNodes.filter(node => node !== nestedList)
|
|
235
|
+
const value = normalizeInlineText(valueNodes.map(readInlineText).join(' '))
|
|
236
|
+
|
|
237
|
+
let label = ''
|
|
238
|
+
if (nestedList && Array.isArray(nestedList.children)) {
|
|
239
|
+
const firstNestedLi = nestedList.children
|
|
240
|
+
.filter(isVNode)
|
|
241
|
+
.find(node => node.type === 'li')
|
|
242
|
+
|
|
243
|
+
if (firstNestedLi)
|
|
244
|
+
label = readInlineText(firstNestedLi)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { value, label }
|
|
248
|
+
})
|
|
249
|
+
.filter(item => item.value)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function shouldUseExplicitRoleChildren(
|
|
253
|
+
children: VNode[],
|
|
254
|
+
shorthandKey: string,
|
|
255
|
+
readRole: (node: VNode) => string,
|
|
256
|
+
) {
|
|
257
|
+
const roles = children
|
|
258
|
+
.map(readRole)
|
|
259
|
+
.filter(Boolean)
|
|
260
|
+
|
|
261
|
+
if (!roles.length)
|
|
262
|
+
return false
|
|
263
|
+
|
|
264
|
+
return shorthandKey !== 'collection:agenda' || roles.some(role => role !== 'media')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function buildAgendaChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
268
|
+
const title = children.find(isHeadingNode)
|
|
269
|
+
const headingTextNode = title
|
|
270
|
+
? null
|
|
271
|
+
: children.find((node) => {
|
|
272
|
+
if (!isTextNode(node))
|
|
273
|
+
return false
|
|
274
|
+
|
|
275
|
+
return extractMarkdownHeading(readTextNode(node)) != null
|
|
276
|
+
})
|
|
277
|
+
const list = children.find(isListNode)
|
|
278
|
+
const mediaOverrides = children.filter(node => contextReadRole(context, node) === 'media')
|
|
279
|
+
|
|
280
|
+
const parsedHeading = headingTextNode ? extractMarkdownHeading(readTextNode(headingTextNode)) : null
|
|
281
|
+
const renderedTitle = title ?? (parsedHeading ? h(`h${parsedHeading.level}`, parsedHeading.text) : null)
|
|
282
|
+
|
|
283
|
+
if (!renderedTitle)
|
|
284
|
+
failMarkdownContract('collection:agenda', 'ожидает один markdown heading внутри default slot.')
|
|
285
|
+
|
|
286
|
+
if (!list)
|
|
287
|
+
failMarkdownContract('collection:agenda', 'ожидает один markdown list (`*`, `-` или `1.`) внутри default slot.')
|
|
288
|
+
|
|
289
|
+
if (mediaOverrides.length > 1)
|
|
290
|
+
failMarkdownContract('collection:agenda', 'поддерживает не больше одного override для `role="media"`.')
|
|
291
|
+
|
|
292
|
+
const mediaOverride = mediaOverrides[0]
|
|
293
|
+
|
|
294
|
+
ensureOnlySupportedChildren('collection:agenda', children, node =>
|
|
295
|
+
node === title
|
|
296
|
+
|| node === headingTextNode
|
|
297
|
+
|| node === list
|
|
298
|
+
|| node === mediaOverride
|
|
299
|
+
|| isBlankTextNode(node),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return [
|
|
303
|
+
h(context.components.Slot, {
|
|
304
|
+
role: 'primary',
|
|
305
|
+
label: `slide-collection-${context.variant}-primary`,
|
|
306
|
+
}, {
|
|
307
|
+
default: withCtx(() => [
|
|
308
|
+
h('div', { class: 'Slide-AgendaContent' }, [
|
|
309
|
+
isVNode(renderedTitle)
|
|
310
|
+
? cloneVNode(renderedTitle, { class: 'Slide-AgendaTitle' })
|
|
311
|
+
: renderedTitle,
|
|
312
|
+
cloneVNode(list, { class: 'Slide-AgendaList' }),
|
|
313
|
+
]),
|
|
314
|
+
]),
|
|
315
|
+
}),
|
|
316
|
+
mediaOverride ?? h(context.components.Slot, {
|
|
317
|
+
role: 'media',
|
|
318
|
+
label: `slide-collection-${context.variant}-media`,
|
|
319
|
+
decor: context.agendaDecor,
|
|
320
|
+
}),
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function contextReadRole(context: LayoutShorthandContext, node: VNode) {
|
|
325
|
+
if (!isNamedComponentVNode(node, 'Slot', context.components.Slot))
|
|
326
|
+
return ''
|
|
327
|
+
|
|
328
|
+
const role = readVNodeProp<string>(node, 'role')
|
|
329
|
+
return typeof role === 'string' ? role.trim() : ''
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function normalizeQuotePersonProps(context: LayoutShorthandContext) {
|
|
333
|
+
const person = context.readFrontmatterObject('person')
|
|
334
|
+
if (!person)
|
|
335
|
+
failMarkdownContract('message:quote', 'ожидает frontmatter person.')
|
|
336
|
+
|
|
337
|
+
const name = readRecordText(person, ['name'])
|
|
338
|
+
if (!name)
|
|
339
|
+
failMarkdownContract('message:quote', 'ожидает person.name во frontmatter.')
|
|
340
|
+
|
|
341
|
+
const personProps: Record<string, string | number> = { name }
|
|
342
|
+
const textProps = [
|
|
343
|
+
['title', ['title']],
|
|
344
|
+
['avatar', ['avatar']],
|
|
345
|
+
['avatarFit', ['avatarFit', 'avatar-fit']],
|
|
346
|
+
['avatarPosition', ['avatarPosition', 'avatar-position']],
|
|
347
|
+
['avatarAnchor', ['avatarAnchor', 'avatar-anchor']],
|
|
348
|
+
] as const
|
|
349
|
+
|
|
350
|
+
for (const [prop, keys] of textProps) {
|
|
351
|
+
const value = readRecordText(person, keys)
|
|
352
|
+
if (value)
|
|
353
|
+
personProps[prop] = value
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const numericInputProps = [
|
|
357
|
+
['avatarX', ['avatarX', 'avatar-x']],
|
|
358
|
+
['avatarY', ['avatarY', 'avatar-y']],
|
|
359
|
+
['avatarZoom', ['avatarZoom', 'avatar-zoom']],
|
|
360
|
+
] as const
|
|
361
|
+
|
|
362
|
+
for (const [prop, keys] of numericInputProps) {
|
|
363
|
+
const value = readRecordInput(person, keys)
|
|
364
|
+
if (value !== undefined)
|
|
365
|
+
personProps[prop] = value
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return personProps
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function buildQuoteChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
372
|
+
const quote = children.find(isBlockquoteNode)
|
|
373
|
+
const quoteTextNode = quote
|
|
374
|
+
? null
|
|
375
|
+
: children.find((node) => {
|
|
376
|
+
if (!isTextNode(node))
|
|
377
|
+
return false
|
|
378
|
+
|
|
379
|
+
return extractMarkdownQuote(readTextNode(node)) != null
|
|
380
|
+
})
|
|
381
|
+
const personProps = normalizeQuotePersonProps(context)
|
|
382
|
+
|
|
383
|
+
const parsedQuote = quoteTextNode ? extractMarkdownQuote(readTextNode(quoteTextNode)) : null
|
|
384
|
+
const renderedQuote = quote ?? (parsedQuote
|
|
385
|
+
? h('blockquote', [h('p', parsedQuote)])
|
|
386
|
+
: null)
|
|
387
|
+
|
|
388
|
+
if (!renderedQuote)
|
|
389
|
+
failMarkdownContract('message:quote', 'ожидает один blockquote (`> ...`) внутри default slot.')
|
|
390
|
+
|
|
391
|
+
ensureOnlySupportedChildren('message:quote', children, node =>
|
|
392
|
+
node === quote
|
|
393
|
+
|| node === quoteTextNode
|
|
394
|
+
|| isBlankTextNode(node),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
return [
|
|
398
|
+
h(context.components.Slot, {
|
|
399
|
+
role: 'primary',
|
|
400
|
+
label: `slide-message-${context.variant}-primary`,
|
|
401
|
+
}, {
|
|
402
|
+
default: withCtx(() => [
|
|
403
|
+
isVNode(renderedQuote)
|
|
404
|
+
? cloneVNode(renderedQuote, { class: 'Slide-Quote' })
|
|
405
|
+
: renderedQuote,
|
|
406
|
+
]),
|
|
407
|
+
}),
|
|
408
|
+
h(context.components.Slot, {
|
|
409
|
+
role: 'support',
|
|
410
|
+
label: `slide-message-${context.variant}-support`,
|
|
411
|
+
}, {
|
|
412
|
+
default: withCtx(() => [
|
|
413
|
+
h(context.components.Person, {
|
|
414
|
+
...personProps,
|
|
415
|
+
align: 'end',
|
|
416
|
+
}),
|
|
417
|
+
]),
|
|
418
|
+
}),
|
|
419
|
+
]
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function buildCenteredChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
423
|
+
const title = readHeadingText(children, 'message:centered')
|
|
424
|
+
|
|
425
|
+
ensureOnlySupportedChildren('message:centered', children, node =>
|
|
426
|
+
node === title.node
|
|
427
|
+
|| isBlankTextNode(node),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
return [
|
|
431
|
+
hSlot(context, 'primary', 'slide-message-centered-primary', { centered: true }, [
|
|
432
|
+
hText(context, 'h1', '7', title.text, { align: 'middle' }),
|
|
433
|
+
]),
|
|
434
|
+
]
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function buildClosingChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
438
|
+
const title = readHeadingText(children, 'message:closing')
|
|
439
|
+
|
|
440
|
+
ensureOnlySupportedChildren('message:closing', children, node =>
|
|
441
|
+
node === title.node
|
|
442
|
+
|| isBlankTextNode(node),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
return [
|
|
446
|
+
hSlot(context, 'primary', 'slide-message-closing-primary', { centered: true }, [
|
|
447
|
+
hText(context, 'h1', '7', title.text, { align: 'middle' }),
|
|
448
|
+
]),
|
|
449
|
+
]
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function buildDefinitionChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
453
|
+
const title = readHeadingText(children, 'explainer:definition')
|
|
454
|
+
const paragraph = readParagraphText(children)
|
|
455
|
+
const body = context.readFrontmatterText('body') || paragraph?.text || ''
|
|
456
|
+
const label = context.readFrontmatterText('label')
|
|
457
|
+
|
|
458
|
+
if (!body)
|
|
459
|
+
failMarkdownContract('explainer:definition', 'ожидает markdown paragraph или frontmatter body.')
|
|
460
|
+
|
|
461
|
+
ensureOnlySupportedChildren('explainer:definition', children, node =>
|
|
462
|
+
node === title.node
|
|
463
|
+
|| node === paragraph?.node
|
|
464
|
+
|| isBlankTextNode(node),
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
return [
|
|
468
|
+
hSlot(context, 'primary', 'slide-explainer-definition-primary', {}, [
|
|
469
|
+
...(label ? [hText(context, 'div', '3', label, { muted: true, className: 'Slide-DefinitionLabel' })] : []),
|
|
470
|
+
hText(context, 'h1', '6', title.text, { priority: 1 }),
|
|
471
|
+
hText(context, 'div', '6', body, { muted: true, priority: 1 }),
|
|
472
|
+
]),
|
|
473
|
+
]
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function buildTitleSupportsChildren(
|
|
477
|
+
children: VNode[],
|
|
478
|
+
context: LayoutShorthandContext,
|
|
479
|
+
options: { placement?: 'side' | 'bottom', muted?: boolean, supportSize?: ThemeTextSizeInput } = {},
|
|
480
|
+
) {
|
|
481
|
+
const title = readHeadingText(children, 'explainer:title-supports')
|
|
482
|
+
const list = children.find(isListNode)
|
|
483
|
+
const placement = options.placement ?? 'side'
|
|
484
|
+
const muted = options.muted ?? true
|
|
485
|
+
const supportSize = options.supportSize ?? '4'
|
|
486
|
+
|
|
487
|
+
if (!list)
|
|
488
|
+
failMarkdownContract('explainer:title-supports', 'ожидает ненумерованный markdown list под heading.')
|
|
489
|
+
|
|
490
|
+
if (list.type !== 'ul')
|
|
491
|
+
failMarkdownContract('explainer:title-supports', 'ожидает ненумерованный markdown list (`-` или `*`).')
|
|
492
|
+
|
|
493
|
+
const supports = readListItems(list, 'explainer:title-supports')
|
|
494
|
+
if (!supports.length)
|
|
495
|
+
failMarkdownContract('explainer:title-supports', 'ожидает list items с текстом.')
|
|
496
|
+
|
|
497
|
+
if (placement === 'bottom' && supports.length !== 2)
|
|
498
|
+
failMarkdownContract('explainer:title-supports', 'ожидает ровно два list items для нижнего 50/50 layout.')
|
|
499
|
+
|
|
500
|
+
ensureOnlySupportedChildren('explainer:title-supports', children, node =>
|
|
501
|
+
node === title.node
|
|
502
|
+
|| node === list
|
|
503
|
+
|| isBlankTextNode(node),
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
if (placement === 'bottom') {
|
|
507
|
+
return [
|
|
508
|
+
hSlot(context, 'primary', 'slide-explainer-title-supports-bottom-primary', {}, [
|
|
509
|
+
hText(context, 'h1', '6', title.text),
|
|
510
|
+
]),
|
|
511
|
+
...supports.map((item, index) =>
|
|
512
|
+
hSlot(context, 'support', `slide-explainer-title-supports-bottom-support-${index + 1}`, {}, [
|
|
513
|
+
hText(context, 'div', supportSize, item, { muted }),
|
|
514
|
+
]),
|
|
515
|
+
),
|
|
516
|
+
]
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return [
|
|
520
|
+
hSlot(context, 'primary', 'slide-explainer-title-supports-primary', {}, [
|
|
521
|
+
hText(context, 'h1', '6', title.text),
|
|
522
|
+
]),
|
|
523
|
+
hSlot(context, 'secondary', 'slide-explainer-title-supports-secondary', {}, [
|
|
524
|
+
cloneVNode(list, { class: 'Slide-TitleSupportsList Slide-TitleSupportsList_size_large' }),
|
|
525
|
+
]),
|
|
526
|
+
]
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function isRichTextBodyNode(node: VNode) {
|
|
530
|
+
return isParagraphNode(node) || isListNode(node) || isBlockquoteNode(node)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function buildTitleBodyChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
534
|
+
const title = readHeadingText(children, 'explainer:title-body')
|
|
535
|
+
const bodyNodes = children.filter(node =>
|
|
536
|
+
node !== title.node
|
|
537
|
+
&& !isBlankTextNode(node),
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
if (!bodyNodes.length)
|
|
541
|
+
failMarkdownContract('explainer:title-body', 'ожидает markdown paragraph, list или blockquote под heading.')
|
|
542
|
+
|
|
543
|
+
ensureOnlySupportedChildren('explainer:title-body', children, node =>
|
|
544
|
+
node === title.node
|
|
545
|
+
|| isRichTextBodyNode(node)
|
|
546
|
+
|| isBlankTextNode(node),
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
return [
|
|
550
|
+
hSlot(context, 'primary', 'slide-explainer-title-body-primary', {}, [
|
|
551
|
+
hText(context, 'h1', '6', title.text),
|
|
552
|
+
]),
|
|
553
|
+
hSlot(context, 'secondary', 'slide-explainer-title-body-secondary', {}, [
|
|
554
|
+
h('div', { class: 'Slide-TitleBody' }, bodyNodes.map(node => cloneVNode(node))),
|
|
555
|
+
]),
|
|
556
|
+
]
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function buildPointsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
560
|
+
const title = readHeadingText(children, 'collection:points')
|
|
561
|
+
const list = children.find(isListNode)
|
|
562
|
+
|
|
563
|
+
if (!list)
|
|
564
|
+
failMarkdownContract('collection:points', 'ожидает markdown list с тремя пунктами.')
|
|
565
|
+
|
|
566
|
+
const items = readListItems(list, 'collection:points')
|
|
567
|
+
if (items.length !== 3)
|
|
568
|
+
failMarkdownContract('collection:points', 'ожидает ровно три пункта для arrangement: trio.')
|
|
569
|
+
|
|
570
|
+
ensureOnlySupportedChildren('collection:points', children, node =>
|
|
571
|
+
node === title.node
|
|
572
|
+
|| node === list
|
|
573
|
+
|| isBlankTextNode(node),
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
return [
|
|
577
|
+
hSlot(context, 'primary', 'slide-collection-points-primary', {
|
|
578
|
+
...(context.frontmatterDecor ? { decor: context.frontmatterDecor } : {}),
|
|
579
|
+
}, [
|
|
580
|
+
hText(context, 'h1', '7', title.text, { className: 'Slide-PointsTitle' }),
|
|
581
|
+
]),
|
|
582
|
+
...items.map((item, index) =>
|
|
583
|
+
hSlot(context, 'support', `slide-collection-points-support-${index + 1}`, {}, [
|
|
584
|
+
hText(context, 'div', '4', String(index + 1)),
|
|
585
|
+
hText(context, 'div', '5', item, { align: 'end' }),
|
|
586
|
+
]),
|
|
587
|
+
),
|
|
588
|
+
]
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function normalizeTimelineItems(context: LayoutShorthandContext) {
|
|
592
|
+
const items = context.readFrontmatterArray('items')
|
|
593
|
+
|
|
594
|
+
if (!items.length)
|
|
595
|
+
failMarkdownContract('collection:timeline', 'ожидает frontmatter items.')
|
|
596
|
+
|
|
597
|
+
return items.map((item, index) => {
|
|
598
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
599
|
+
failMarkdownContract('collection:timeline', `ожидает object item в items[${index}].`)
|
|
600
|
+
|
|
601
|
+
const record = item as Record<string, unknown>
|
|
602
|
+
const year = readRecordText(record, ['year', 'label'])
|
|
603
|
+
const label = readRecordText(record, ['body', 'label', 'title'])
|
|
604
|
+
|
|
605
|
+
if (!year || !label)
|
|
606
|
+
failMarkdownContract('collection:timeline', `ожидает year и body/label в items[${index}].`)
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
year,
|
|
610
|
+
label,
|
|
611
|
+
active: record.active === true,
|
|
612
|
+
}
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function buildTimelineChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
617
|
+
const title = readHeadingText(children, 'collection:timeline')
|
|
618
|
+
const paragraph = readParagraphText(children)
|
|
619
|
+
const body = context.readFrontmatterText('body') || paragraph?.text || ''
|
|
620
|
+
|
|
621
|
+
ensureOnlySupportedChildren('collection:timeline', children, node =>
|
|
622
|
+
node === title.node
|
|
623
|
+
|| node === paragraph?.node
|
|
624
|
+
|| isBlankTextNode(node),
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
return [
|
|
628
|
+
hSlot(context, 'primary', 'slide-collection-timeline-primary', {}, [
|
|
629
|
+
hText(context, 'h1', '6', title.text),
|
|
630
|
+
]),
|
|
631
|
+
...(body
|
|
632
|
+
? [
|
|
633
|
+
hSlot(context, 'secondary', 'slide-collection-timeline-secondary', {}, [
|
|
634
|
+
hText(context, 'div', '3', body, { muted: true }),
|
|
635
|
+
]),
|
|
636
|
+
]
|
|
637
|
+
: []),
|
|
638
|
+
hSlot(context, 'support', 'slide-collection-timeline-support', {}, [
|
|
639
|
+
h(context.components.Timeline, { items: normalizeTimelineItems(context) }),
|
|
640
|
+
]),
|
|
641
|
+
]
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function normalizeMetricItems(context: LayoutShorthandContext) {
|
|
645
|
+
const items = context.readFrontmatterArray('metrics')
|
|
646
|
+
|
|
647
|
+
if (items.length < 3)
|
|
648
|
+
failMarkdownContract('collection:metrics', 'ожидает минимум три элемента во frontmatter metrics.')
|
|
649
|
+
|
|
650
|
+
return items.map((item, index) => {
|
|
651
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
652
|
+
failMarkdownContract('collection:metrics', `ожидает object item в metrics[${index}].`)
|
|
653
|
+
|
|
654
|
+
const record = item as Record<string, unknown>
|
|
655
|
+
const value = readRecordText(record, ['value'])
|
|
656
|
+
const label = readRecordText(record, ['body', 'label', 'title'])
|
|
657
|
+
|
|
658
|
+
if (!value || !label)
|
|
659
|
+
failMarkdownContract('collection:metrics', `ожидает value и body/label в metrics[${index}].`)
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
value,
|
|
663
|
+
label,
|
|
664
|
+
featured: record.featured === true,
|
|
665
|
+
}
|
|
666
|
+
})
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function normalizeMetricItemsFromList(list: VNode) {
|
|
670
|
+
const entries = readNestedListItems(list, 'collection:metrics')
|
|
671
|
+
|
|
672
|
+
if (entries.length < 3)
|
|
673
|
+
failMarkdownContract('collection:metrics', 'ожидает минимум три пункта в markdown-списке.')
|
|
674
|
+
|
|
675
|
+
return entries.map((entry, index) => {
|
|
676
|
+
if (!entry.label)
|
|
677
|
+
failMarkdownContract('collection:metrics', `ожидает вложенный пункт с подписью у metric[${index}].`)
|
|
678
|
+
|
|
679
|
+
return {
|
|
680
|
+
value: entry.value,
|
|
681
|
+
label: entry.label,
|
|
682
|
+
featured: index === 0,
|
|
683
|
+
}
|
|
684
|
+
})
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function normalizeMediaItems(context: LayoutShorthandContext) {
|
|
688
|
+
const value = context.readFrontmatterValue('media')
|
|
689
|
+
|
|
690
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value))
|
|
691
|
+
return [value as Record<string, unknown>]
|
|
692
|
+
|
|
693
|
+
if (Array.isArray(value)) {
|
|
694
|
+
return value.map((item, index) => {
|
|
695
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
696
|
+
failMarkdownContract('collection:metrics', `ожидает object item в media[${index}].`)
|
|
697
|
+
|
|
698
|
+
return item as Record<string, unknown>
|
|
699
|
+
})
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return []
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
706
|
+
const list = children.find(isListNode)
|
|
707
|
+
|
|
708
|
+
ensureOnlySupportedChildren('collection:metrics', children, node =>
|
|
709
|
+
node === list
|
|
710
|
+
|| isBlankTextNode(node),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
const isFeaturedCopy = context.variant === 'metrics-featured-copy' || context.variant === 'metrics-featured-copy-split-media'
|
|
714
|
+
const isSplitMedia = context.variant === 'metrics-featured-copy-split-media'
|
|
715
|
+
const mediaItems = normalizeMediaItems(context)
|
|
716
|
+
|
|
717
|
+
if (!mediaItems.length)
|
|
718
|
+
failMarkdownContract('collection:metrics', 'ожидает frontmatter media.')
|
|
719
|
+
|
|
720
|
+
if (isSplitMedia && mediaItems.length < 2)
|
|
721
|
+
failMarkdownContract('collection:metrics', 'ожидает два frontmatter media item для arrangement: featured-copy-split-media.')
|
|
722
|
+
|
|
723
|
+
const metrics = list
|
|
724
|
+
? normalizeMetricItemsFromList(list)
|
|
725
|
+
: normalizeMetricItems(context)
|
|
726
|
+
const featuredIndex = metrics.findIndex(metric => metric.featured)
|
|
727
|
+
const featured = metrics[featuredIndex >= 0 ? featuredIndex : 0]
|
|
728
|
+
const supportMetrics = metrics.filter(metric => metric !== featured).slice(0, 2)
|
|
729
|
+
|
|
730
|
+
if (supportMetrics.length < 2)
|
|
731
|
+
failMarkdownContract('collection:metrics', 'ожидает два вторичных metric item.')
|
|
732
|
+
|
|
733
|
+
const primaryChildren = isFeaturedCopy
|
|
734
|
+
? [
|
|
735
|
+
h('div', { class: 'Slide-MetricsFeaturedCopy' }, [
|
|
736
|
+
h('div', { class: 'Slide-MetricsFeaturedCopyValue' }, [
|
|
737
|
+
hText(context, 'div', '7-12', featured.value, { priority: 1, align: 'middle' }),
|
|
738
|
+
]),
|
|
739
|
+
h('div', { class: 'Slide-MetricsFeaturedCopyLabel' }, [
|
|
740
|
+
hText(context, 'div', '5-7', featured.label, { priority: 1, align: 'middle', muted: true }),
|
|
741
|
+
]),
|
|
742
|
+
]),
|
|
743
|
+
]
|
|
744
|
+
: [
|
|
745
|
+
hText(context, 'div', '7-12', featured.value, { priority: 1, className: 'Slide-MetricsFeaturedText' }),
|
|
746
|
+
hText(context, 'div', '2-7', featured.label, { priority: 2, muted: true, className: 'Slide-MetricsFeaturedText' }),
|
|
747
|
+
]
|
|
748
|
+
const mediaCount = isSplitMedia ? 2 : 1
|
|
749
|
+
|
|
750
|
+
return [
|
|
751
|
+
hSlot(context, 'primary', 'slide-collection-metrics-primary', {
|
|
752
|
+
...(!isFeaturedCopy && context.frontmatterDecor ? { decor: context.frontmatterDecor } : {}),
|
|
753
|
+
gap: '3',
|
|
754
|
+
}, primaryChildren),
|
|
755
|
+
...mediaItems.slice(0, mediaCount).map((media, index) =>
|
|
756
|
+
hSlot(context, 'media', index === 0 ? 'slide-collection-metrics-media' : `slide-collection-metrics-media-${index + 1}`, {}, [
|
|
757
|
+
h(context.components.Image, media),
|
|
758
|
+
]),
|
|
759
|
+
),
|
|
760
|
+
...supportMetrics.map((metric, index) =>
|
|
761
|
+
hSlot(context, 'support', `slide-collection-metrics-support-${index + 1}`, { gap: '2' }, [
|
|
762
|
+
hText(context, 'div', '7', metric.value, { priority: 1 }),
|
|
763
|
+
hText(context, 'div', '2-6', metric.label, { priority: 2, muted: true, typography: 'compact' }),
|
|
764
|
+
]),
|
|
765
|
+
),
|
|
766
|
+
]
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function buildFactsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
770
|
+
const heading = children.find(isHeadingNode)
|
|
771
|
+
const list = children.find(isListNode)
|
|
772
|
+
const paragraphs = children.filter(isParagraphNode)
|
|
773
|
+
|
|
774
|
+
if (!heading)
|
|
775
|
+
failMarkdownContract('collection:facts', 'ожидает один markdown heading внутри default slot.')
|
|
776
|
+
|
|
777
|
+
if (!list)
|
|
778
|
+
failMarkdownContract('collection:facts', 'ожидает один markdown список с фактами внутри default slot.')
|
|
779
|
+
|
|
780
|
+
const headingIndex = children.indexOf(heading)
|
|
781
|
+
const listIndex = children.indexOf(list)
|
|
782
|
+
const description = paragraphs.find((node) => {
|
|
783
|
+
const index = children.indexOf(node)
|
|
784
|
+
return index > headingIndex && index < listIndex
|
|
785
|
+
})
|
|
786
|
+
const footnote = paragraphs.find((node) => {
|
|
787
|
+
const index = children.indexOf(node)
|
|
788
|
+
return index > listIndex
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
ensureOnlySupportedChildren('collection:facts', children, node =>
|
|
792
|
+
node === heading
|
|
793
|
+
|| node === list
|
|
794
|
+
|| node === description
|
|
795
|
+
|| node === footnote
|
|
796
|
+
|| isBlankTextNode(node),
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
const facts = readNestedListItems(list, 'collection:facts')
|
|
800
|
+
|
|
801
|
+
if (facts.length < 3)
|
|
802
|
+
failMarkdownContract('collection:facts', 'ожидает минимум три факта в markdown-списке.')
|
|
803
|
+
|
|
804
|
+
facts.forEach((fact, index) => {
|
|
805
|
+
if (!fact.label)
|
|
806
|
+
failMarkdownContract('collection:facts', `ожидает вложенный пункт с подписью у facts[${index}].`)
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
const [featured, ...rest] = facts
|
|
810
|
+
const supportFacts = rest.slice(0, 2)
|
|
811
|
+
|
|
812
|
+
if (supportFacts.length < 2)
|
|
813
|
+
failMarkdownContract('collection:facts', 'ожидает минимум два дополнительных факта в markdown-списке.')
|
|
814
|
+
|
|
815
|
+
const isFeatured = context.variant === 'facts-featured'
|
|
816
|
+
const isStacked = context.variant === 'facts-stacked'
|
|
817
|
+
const headingText = readInlineText(heading)
|
|
818
|
+
const descriptionText = description ? readInlineText(description) : ''
|
|
819
|
+
const footnoteText = footnote ? readInlineText(footnote) : ''
|
|
820
|
+
|
|
821
|
+
const primaryChildren: VNode[] = [
|
|
822
|
+
hText(context, 'h2', isFeatured ? '5-7' : '6-8', headingText, { priority: 1 }),
|
|
823
|
+
]
|
|
824
|
+
|
|
825
|
+
if (descriptionText)
|
|
826
|
+
primaryChildren.push(hText(context, 'p', isStacked ? '4-5' : '3-4', descriptionText, { priority: 2, muted: true }))
|
|
827
|
+
|
|
828
|
+
const featuredChildren: VNode[] = [
|
|
829
|
+
hText(context, 'div', isFeatured ? '9-12' : '7-10', featured.value, { priority: 1 }),
|
|
830
|
+
hText(context, 'div', isFeatured ? '4-6' : '3-5', featured.label, { priority: 2, muted: true }),
|
|
831
|
+
]
|
|
832
|
+
|
|
833
|
+
if (footnoteText)
|
|
834
|
+
featuredChildren.push(hText(context, 'div', '2-3', footnoteText, { priority: 3, muted: true }))
|
|
835
|
+
|
|
836
|
+
return [
|
|
837
|
+
hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, { gap: '3' }, primaryChildren),
|
|
838
|
+
hSlot(context, 'support', `slide-collection-${context.variant}-support-1`, {
|
|
839
|
+
gap: '2',
|
|
840
|
+
centered: false,
|
|
841
|
+
}, featuredChildren),
|
|
842
|
+
...supportFacts.map((fact, index) =>
|
|
843
|
+
hSlot(context, 'support', `slide-collection-${context.variant}-support-${index + 2}`, { gap: '2' }, [
|
|
844
|
+
hText(context, 'div', isFeatured ? '5-7' : '7-9', fact.value, { priority: 1 }),
|
|
845
|
+
hText(context, 'div', '2-4', fact.label, { priority: 2, muted: true }),
|
|
846
|
+
]),
|
|
847
|
+
),
|
|
848
|
+
]
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const LAYOUT_SHORTHANDS = {
|
|
852
|
+
'collection:agenda': buildAgendaChildren,
|
|
853
|
+
'collection:facts-stacked': buildFactsChildren,
|
|
854
|
+
'collection:facts-trio': buildFactsChildren,
|
|
855
|
+
'collection:facts-featured': buildFactsChildren,
|
|
856
|
+
'collection:points-trio': buildPointsChildren,
|
|
857
|
+
'collection:timeline': buildTimelineChildren,
|
|
858
|
+
'collection:metrics-featured-media': buildMetricsChildren,
|
|
859
|
+
'collection:metrics-featured-copy': buildMetricsChildren,
|
|
860
|
+
'collection:metrics-featured-copy-split-media': buildMetricsChildren,
|
|
861
|
+
'message:centered': buildCenteredChildren,
|
|
862
|
+
'message:quote': buildQuoteChildren,
|
|
863
|
+
'message:closing': buildClosingChildren,
|
|
864
|
+
'explainer:definition': buildDefinitionChildren,
|
|
865
|
+
'explainer:title-supports': buildTitleSupportsChildren,
|
|
866
|
+
'explainer:title-supports-bottom-muted': (children: VNode[], context: LayoutShorthandContext) =>
|
|
867
|
+
buildTitleSupportsChildren(children, context, { placement: 'bottom', muted: true, supportSize: '5' }),
|
|
868
|
+
'explainer:title-supports-bottom-plain': (children: VNode[], context: LayoutShorthandContext) =>
|
|
869
|
+
buildTitleSupportsChildren(children, context, { placement: 'bottom', muted: false, supportSize: '3' }),
|
|
870
|
+
'explainer:title-body': buildTitleBodyChildren,
|
|
871
|
+
} satisfies Record<string, LayoutShorthandBuilder>
|
|
872
|
+
|
|
873
|
+
type LayoutShorthandKey = keyof typeof LAYOUT_SHORTHANDS
|
|
874
|
+
|
|
875
|
+
function isLayoutShorthandKey(key: string): key is LayoutShorthandKey {
|
|
876
|
+
return key in LAYOUT_SHORTHANDS
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export function hasLayoutShorthand(key: string) {
|
|
880
|
+
return isLayoutShorthandKey(key)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export function buildLayoutShorthandChildren(
|
|
884
|
+
key: string,
|
|
885
|
+
children: VNode[],
|
|
886
|
+
context: LayoutShorthandContext,
|
|
887
|
+
) {
|
|
888
|
+
return isLayoutShorthandKey(key)
|
|
889
|
+
? LAYOUT_SHORTHANDS[key](children, context)
|
|
890
|
+
: null
|
|
891
|
+
}
|