slidev-theme-practicum 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +72 -10
- package/components/Slide.vue +81 -26
- package/components/Slot.vue +33 -48
- package/components/Text.vue +68 -2
- package/components/Timeline.vue +2 -23
- package/composables/deck-slot-markup.cjs +249 -0
- package/composables/decor-catalog.mjs +169 -47
- package/composables/layout-authoring.ts +6 -2
- package/composables/layout-markdown.mjs +33 -0
- package/composables/layout-recipes.ts +51 -5
- package/composables/layout-shorthands.ts +251 -81
- package/composables/markdown-to-vnodes.cjs +1 -0
- package/composables/markdown-to-vnodes.mjs +237 -0
- package/composables/slot-placement.ts +1 -1
- package/composables/slot-spacing.ts +66 -0
- package/composables/text-fit-runtime.ts +458 -76
- package/composables/text-fit-scope.ts +13 -0
- package/composables/text-slot-markdown.mjs +193 -0
- package/composables/text-typography.mjs +2 -0
- package/composables/theme-assets.mjs +60 -0
- package/composables/timeline-grid.ts +27 -0
- package/composables/typescript-require.cjs +29 -0
- package/composables/validate-deck-layouts.cjs +277 -0
- package/decor-library.md +2 -0
- package/example.md +132 -10
- package/package.json +25 -11
- package/public/photos/photo-1.webp +0 -0
- package/public/photos/photo-10.webp +0 -0
- package/public/photos/photo-11.webp +0 -0
- package/public/photos/photo-12.webp +0 -0
- package/public/photos/photo-13.webp +0 -0
- package/public/photos/photo-14.webp +0 -0
- package/public/photos/photo-15.webp +0 -0
- package/public/photos/photo-16.webp +0 -0
- package/public/photos/photo-17.webp +0 -0
- package/public/photos/photo-18.webp +0 -0
- package/public/photos/photo-19.webp +0 -0
- package/public/photos/photo-2.webp +0 -0
- package/public/photos/photo-20.webp +0 -0
- package/public/photos/photo-21.webp +0 -0
- package/public/photos/photo-22.webp +0 -0
- package/public/photos/photo-23.webp +0 -0
- package/public/photos/photo-24.webp +0 -0
- package/public/photos/photo-25.webp +0 -0
- package/public/photos/photo-26.webp +0 -0
- package/public/photos/photo-27.webp +0 -0
- package/public/photos/photo-28.webp +0 -0
- package/public/photos/photo-29.webp +0 -0
- package/public/photos/photo-3.webp +0 -0
- package/public/photos/photo-30.webp +0 -0
- package/public/photos/photo-4.webp +0 -0
- package/public/photos/photo-5.webp +0 -0
- package/public/photos/photo-6.webp +0 -0
- package/public/photos/photo-7.webp +0 -0
- package/public/photos/photo-8.webp +0 -0
- package/public/photos/photo-9.webp +0 -0
- package/scripts/browser-smoke-runtime.mjs +428 -0
- package/scripts/browser-smoke.mjs +424 -0
- package/scripts/build-artifact-assets.mjs +55 -0
- package/scripts/check-build-artifact.mjs +243 -0
- package/scripts/check-package.mjs +132 -0
- package/scripts/check-test-architecture.mjs +176 -0
- package/scripts/npm-spawn.mjs +31 -0
- package/scripts/test-architecture-policy.mjs +245 -0
- package/scripts/theme-asset-inventory.mjs +83 -0
- package/scripts/validate-deck.cjs +18 -0
- package/setup/decor-save-middleware.ts +244 -0
- package/setup/vite-plugins.ts +14 -84
- package/skills/slidev-practicum/SKILL.md +25 -0
- package/skills/slidev-practicum/agents/openai.yaml +4 -0
- package/styles/index.css +10 -1
- package/composables/layout-registry.ts +0 -47
- package/env.d.ts +0 -6
- 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/slidev-theme-practicum.png +0 -0
- package/slidev-theme-practicum.svg +0 -102
|
@@ -167,8 +167,7 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
167
167
|
content: 'closing',
|
|
168
168
|
aliases: [{ variant: 'closing' }],
|
|
169
169
|
regions: {
|
|
170
|
-
primary: { area: '
|
|
171
|
-
support: '12 / 1 / -1 / 4',
|
|
170
|
+
primary: { area: '1 / 2 / -1 / 12', centered: true },
|
|
172
171
|
},
|
|
173
172
|
},
|
|
174
173
|
]),
|
|
@@ -177,7 +176,7 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
177
176
|
content: 'title-supports',
|
|
178
177
|
aliases: [{ variant: 'title-supports' }],
|
|
179
178
|
regions: {
|
|
180
|
-
primary: '2 / 1 / -1 / 7',
|
|
179
|
+
primary: { area: '2 / 1 / -1 / 7', marginRight: 4 },
|
|
181
180
|
secondary: '2 / 7 / -1 / -1',
|
|
182
181
|
},
|
|
183
182
|
},
|
|
@@ -224,8 +223,8 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
224
223
|
content: 'agenda',
|
|
225
224
|
aliases: [{ variant: 'agenda' }],
|
|
226
225
|
regions: {
|
|
227
|
-
primary: { area: '1 / 1 / -1 /
|
|
228
|
-
media: '1 /
|
|
226
|
+
primary: { area: '1 / 1 / -1 / 10', margin: 4 },
|
|
227
|
+
media: '1 / 10 / -1 / -1',
|
|
229
228
|
},
|
|
230
229
|
},
|
|
231
230
|
{
|
|
@@ -312,6 +311,30 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
312
311
|
],
|
|
313
312
|
},
|
|
314
313
|
},
|
|
314
|
+
{
|
|
315
|
+
content: 'facts',
|
|
316
|
+
aliases: [{ variant: 'facts-duo' }, { variant: 'facts', arrangement: 'duo' }],
|
|
317
|
+
regions: {
|
|
318
|
+
primary: { area: '2 / 1 / 6 / -1' },
|
|
319
|
+
support: [
|
|
320
|
+
{ area: '6 / 1 / -1 / 7', surface: 'light', margin: 3 },
|
|
321
|
+
{ area: '6 / 7 / -1 / -1', surface: 'light', margin: 3 },
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
content: 'facts',
|
|
327
|
+
aliases: [{ variant: 'facts-quartet' }, { variant: 'facts', arrangement: 'quartet' }],
|
|
328
|
+
regions: {
|
|
329
|
+
primary: { area: '2 / 1 / 6 / -1' },
|
|
330
|
+
support: [
|
|
331
|
+
{ area: '6 / 1 / -1 / 4', surface: 'light', margin: 3 },
|
|
332
|
+
{ area: '6 / 4 / -1 / 7', surface: 'light', margin: 3 },
|
|
333
|
+
{ area: '6 / 7 / -1 / 10', surface: 'light', margin: 3 },
|
|
334
|
+
{ area: '6 / 10 / -1 / -1', surface: 'light', margin: 3 },
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
315
338
|
{
|
|
316
339
|
content: 'facts',
|
|
317
340
|
aliases: [{ variant: 'facts-featured' }, { variant: 'facts', arrangement: 'featured' }],
|
|
@@ -345,3 +368,26 @@ export function resolveLayoutRecipe(layout: ThemeLayout, variant: string, arrang
|
|
|
345
368
|
|
|
346
369
|
return recipe
|
|
347
370
|
}
|
|
371
|
+
|
|
372
|
+
export function resolveLayoutVariant(layout: ThemeLayout | '', variant: string, arrangement = '') {
|
|
373
|
+
if (!layout)
|
|
374
|
+
return variant.trim()
|
|
375
|
+
|
|
376
|
+
return resolveLayoutRecipe(layout, variant, arrangement).aliases[0].variant
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function resolveLayoutSpec(layout: ThemeLayout, variant: string) {
|
|
380
|
+
return normalizeRecipeRegions(resolveLayoutRecipe(layout, variant))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function resolveLayoutSlotSpec(
|
|
384
|
+
layout: ThemeLayout,
|
|
385
|
+
variant: string,
|
|
386
|
+
role: ThemeLayoutRole,
|
|
387
|
+
index: number,
|
|
388
|
+
) {
|
|
389
|
+
const resolved = resolveLayoutSpec(layout, variant).slots[role]?.[index]
|
|
390
|
+
if (!resolved)
|
|
391
|
+
fail('Slide', `Variant "${layout}:${variant}" не поддерживает ${role}[${index}].`)
|
|
392
|
+
return resolved
|
|
393
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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-
|
|
2
|
+
import { extractMarkdownHeading, extractMarkdownQuote, normalizeInlineText } from './layout-markdown.mjs'
|
|
3
|
+
import type { ThemeLayoutRole } from './layout-recipes'
|
|
4
4
|
import { isBlankTextVNode, isNamedComponentVNode, readVNodeProp, readVNodeTypeName } from './layout-vnode'
|
|
5
5
|
import type { ThemeTextSizeInput } from './text-fit-runtime'
|
|
6
6
|
|
|
@@ -25,8 +25,17 @@ export type LayoutShorthandContext = {
|
|
|
25
25
|
|
|
26
26
|
type LayoutShorthandBuilder = (children: VNode[], context: LayoutShorthandContext) => VNode[]
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
export type SlideMarkdownContractError = Error & {
|
|
29
|
+
name: 'SlideMarkdownContractError'
|
|
30
|
+
hint?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function failMarkdownContract(label: string, message: string, hint?: string): never {
|
|
34
|
+
const error = new Error(`[Slide] ${label} ${message}`) as SlideMarkdownContractError
|
|
35
|
+
error.name = 'SlideMarkdownContractError'
|
|
36
|
+
if (hint)
|
|
37
|
+
error.hint = hint
|
|
38
|
+
throw error
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
function isHeadingNode(node: VNode) {
|
|
@@ -53,10 +62,6 @@ function readTextNode(node: VNode) {
|
|
|
53
62
|
return typeof node.children === 'string' ? node.children : ''
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
function normalizeInlineText(value: string) {
|
|
57
|
-
return value.replace(/\s+/g, ' ').trim()
|
|
58
|
-
}
|
|
59
|
-
|
|
60
65
|
function readInlineText(node: VNode): string {
|
|
61
66
|
if (node.type === NativeText)
|
|
62
67
|
return normalizeInlineText(readTextNode(node))
|
|
@@ -80,7 +85,17 @@ function ensureOnlySupportedChildren(label: string, children: VNode[], allow: (n
|
|
|
80
85
|
return
|
|
81
86
|
|
|
82
87
|
const unsupportedLabels = unsupportedNodes.map(readVNodeTypeName).join(', ')
|
|
83
|
-
|
|
88
|
+
const suggestion = label === 'message:centered'
|
|
89
|
+
? 'Оставьте только один markdown-заголовок `# …`. Подзаголовок перенесите в заметки автора или объедините с заголовком.'
|
|
90
|
+
: label === 'message:closing'
|
|
91
|
+
? 'Оставьте один markdown-заголовок `# …` и не более одного markdown-абзаца под ним.'
|
|
92
|
+
: undefined
|
|
93
|
+
|
|
94
|
+
failMarkdownContract(
|
|
95
|
+
label,
|
|
96
|
+
`поддерживает только каноническую markdown-структуру. Неподдерживаемые children: ${unsupportedLabels}.`,
|
|
97
|
+
suggestion,
|
|
98
|
+
)
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
function readHeadingText(children: VNode[], label: string) {
|
|
@@ -203,6 +218,34 @@ function readRecordInput(record: Record<string, unknown>, keys: readonly string[
|
|
|
203
218
|
return undefined
|
|
204
219
|
}
|
|
205
220
|
|
|
221
|
+
function readListItemText(li: VNode) {
|
|
222
|
+
if (typeof li.children === 'string')
|
|
223
|
+
return normalizeInlineText(li.children)
|
|
224
|
+
|
|
225
|
+
if (!Array.isArray(li.children))
|
|
226
|
+
return ''
|
|
227
|
+
|
|
228
|
+
const parts: string[] = []
|
|
229
|
+
|
|
230
|
+
for (const child of li.children) {
|
|
231
|
+
if (!isVNode(child)) {
|
|
232
|
+
const text = String(child ?? '').trim()
|
|
233
|
+
if (text)
|
|
234
|
+
parts.push(text)
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (child.type === 'ul' || child.type === 'ol')
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
const text = readInlineText(child).trim()
|
|
242
|
+
if (text)
|
|
243
|
+
parts.push(text)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return normalizeInlineText(parts.join(' '))
|
|
247
|
+
}
|
|
248
|
+
|
|
206
249
|
function readListItems(list: VNode, label: string) {
|
|
207
250
|
if (!Array.isArray(list.children))
|
|
208
251
|
failMarkdownContract(label, 'ожидает list items с текстом.')
|
|
@@ -210,10 +253,54 @@ function readListItems(list: VNode, label: string) {
|
|
|
210
253
|
return list.children
|
|
211
254
|
.filter(isVNode)
|
|
212
255
|
.filter(node => node.type === 'li')
|
|
213
|
-
.map(
|
|
256
|
+
.map(readListItemText)
|
|
214
257
|
.filter(Boolean)
|
|
215
258
|
}
|
|
216
259
|
|
|
260
|
+
function readListItemNodes(list: VNode, label: string) {
|
|
261
|
+
if (!Array.isArray(list.children))
|
|
262
|
+
failMarkdownContract(label, 'ожидает list items с текстом.')
|
|
263
|
+
|
|
264
|
+
return list.children
|
|
265
|
+
.filter(isVNode)
|
|
266
|
+
.filter(node => node.type === 'li')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function hasListItemContent(li: VNode) {
|
|
270
|
+
if (typeof li.children === 'string')
|
|
271
|
+
return Boolean(normalizeInlineText(li.children))
|
|
272
|
+
|
|
273
|
+
if (!Array.isArray(li.children))
|
|
274
|
+
return false
|
|
275
|
+
|
|
276
|
+
return li.children.some((child) => {
|
|
277
|
+
if (!isVNode(child))
|
|
278
|
+
return Boolean(String(child).trim())
|
|
279
|
+
|
|
280
|
+
if (child.type === 'ul' || child.type === 'ol')
|
|
281
|
+
return Array.isArray(child.children) && child.children.some(node => isVNode(node) && node.type === 'li')
|
|
282
|
+
|
|
283
|
+
return Boolean(readInlineText(child))
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function readListItemContent(li: VNode): VNode[] {
|
|
288
|
+
if (typeof li.children === 'string')
|
|
289
|
+
return normalizeInlineText(li.children) ? [h('p', li.children)] : []
|
|
290
|
+
|
|
291
|
+
if (!Array.isArray(li.children))
|
|
292
|
+
return []
|
|
293
|
+
|
|
294
|
+
return li.children.flatMap((child) => {
|
|
295
|
+
if (!isVNode(child)) {
|
|
296
|
+
const text = String(child).trim()
|
|
297
|
+
return text ? [h('p', text)] : []
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return [cloneVNode(child)]
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
217
304
|
type NestedListItem = { value: string, label: string }
|
|
218
305
|
|
|
219
306
|
function readNestedListItems(list: VNode, label: string): NestedListItem[] {
|
|
@@ -229,10 +316,28 @@ function readNestedListItems(list: VNode, label: string): NestedListItem[] {
|
|
|
229
316
|
return { value: text, label: '' }
|
|
230
317
|
}
|
|
231
318
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
319
|
+
const valueParts: string[] = []
|
|
320
|
+
let nestedList: VNode | undefined
|
|
321
|
+
|
|
322
|
+
for (const child of li.children) {
|
|
323
|
+
if (!isVNode(child)) {
|
|
324
|
+
const text = String(child ?? '').trim()
|
|
325
|
+
if (text)
|
|
326
|
+
valueParts.push(text)
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (child.type === 'ul' || child.type === 'ol') {
|
|
331
|
+
nestedList = child
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const text = readInlineText(child).trim()
|
|
336
|
+
if (text)
|
|
337
|
+
valueParts.push(text)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const value = normalizeInlineText(valueParts.join(' '))
|
|
236
341
|
|
|
237
342
|
let label = ''
|
|
238
343
|
if (nestedList && Array.isArray(nestedList.children)) {
|
|
@@ -241,7 +346,7 @@ function readNestedListItems(list: VNode, label: string): NestedListItem[] {
|
|
|
241
346
|
.find(node => node.type === 'li')
|
|
242
347
|
|
|
243
348
|
if (firstNestedLi)
|
|
244
|
-
label =
|
|
349
|
+
label = readListItemText(firstNestedLi)
|
|
245
350
|
}
|
|
246
351
|
|
|
247
352
|
return { value, label }
|
|
@@ -249,6 +354,14 @@ function readNestedListItems(list: VNode, label: string): NestedListItem[] {
|
|
|
249
354
|
.filter(item => item.value)
|
|
250
355
|
}
|
|
251
356
|
|
|
357
|
+
const FACTS_VARIANT_COUNTS: Record<string, number> = {
|
|
358
|
+
'facts-duo': 2,
|
|
359
|
+
'facts-trio': 3,
|
|
360
|
+
'facts-quartet': 4,
|
|
361
|
+
'facts-stacked': 3,
|
|
362
|
+
'facts-featured': 3,
|
|
363
|
+
}
|
|
364
|
+
|
|
252
365
|
export function shouldUseExplicitRoleChildren(
|
|
253
366
|
children: VNode[],
|
|
254
367
|
shorthandKey: string,
|
|
@@ -436,15 +549,20 @@ function buildCenteredChildren(children: VNode[], context: LayoutShorthandContex
|
|
|
436
549
|
|
|
437
550
|
function buildClosingChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
438
551
|
const title = readHeadingText(children, 'message:closing')
|
|
552
|
+
const paragraph = readParagraphText(children)
|
|
439
553
|
|
|
440
554
|
ensureOnlySupportedChildren('message:closing', children, node =>
|
|
441
555
|
node === title.node
|
|
556
|
+
|| node === paragraph?.node
|
|
442
557
|
|| isBlankTextNode(node),
|
|
443
558
|
)
|
|
444
559
|
|
|
445
560
|
return [
|
|
446
561
|
hSlot(context, 'primary', 'slide-message-closing-primary', { centered: true }, [
|
|
447
562
|
hText(context, 'h1', '7', title.text, { align: 'middle' }),
|
|
563
|
+
...(paragraph
|
|
564
|
+
? [hText(context, 'p', '3', paragraph.text, { muted: true })]
|
|
565
|
+
: []),
|
|
448
566
|
]),
|
|
449
567
|
]
|
|
450
568
|
}
|
|
@@ -473,14 +591,56 @@ function buildDefinitionChildren(children: VNode[], context: LayoutShorthandCont
|
|
|
473
591
|
]
|
|
474
592
|
}
|
|
475
593
|
|
|
594
|
+
function isRichTextBodyNode(node: VNode) {
|
|
595
|
+
return isParagraphNode(node) || isListNode(node) || isBlockquoteNode(node)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function buildTitleRichSideChildren(
|
|
599
|
+
children: VNode[],
|
|
600
|
+
context: LayoutShorthandContext,
|
|
601
|
+
contract: 'explainer:title-body' | 'explainer:title-supports',
|
|
602
|
+
options: { muted?: boolean } = {},
|
|
603
|
+
) {
|
|
604
|
+
const title = readHeadingText(children, contract)
|
|
605
|
+
const bodyNodes = children.filter(node =>
|
|
606
|
+
node !== title.node
|
|
607
|
+
&& !isBlankTextNode(node),
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
if (!bodyNodes.length)
|
|
611
|
+
failMarkdownContract(contract, 'ожидает markdown paragraph, list или blockquote под heading.')
|
|
612
|
+
|
|
613
|
+
ensureOnlySupportedChildren(contract, children, node =>
|
|
614
|
+
node === title.node
|
|
615
|
+
|| isRichTextBodyNode(node)
|
|
616
|
+
|| isBlankTextNode(node),
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
const slotPrefix = contract === 'explainer:title-body' ? 'title-body' : 'title-supports'
|
|
620
|
+
const bodyClass = options.muted ? 'Slide-TitleBody Slide-TitleBody_muted' : 'Slide-TitleBody'
|
|
621
|
+
|
|
622
|
+
return [
|
|
623
|
+
hSlot(context, 'primary', `slide-explainer-${slotPrefix}-primary`, {}, [
|
|
624
|
+
hText(context, 'h1', '6', title.text),
|
|
625
|
+
]),
|
|
626
|
+
hSlot(context, 'secondary', `slide-explainer-${slotPrefix}-secondary`, {}, [
|
|
627
|
+
h('div', { class: bodyClass }, bodyNodes.map(node => cloneVNode(node))),
|
|
628
|
+
]),
|
|
629
|
+
]
|
|
630
|
+
}
|
|
631
|
+
|
|
476
632
|
function buildTitleSupportsChildren(
|
|
477
633
|
children: VNode[],
|
|
478
634
|
context: LayoutShorthandContext,
|
|
479
635
|
options: { placement?: 'side' | 'bottom', muted?: boolean, supportSize?: ThemeTextSizeInput } = {},
|
|
480
636
|
) {
|
|
637
|
+
const placement = options.placement ?? 'side'
|
|
638
|
+
|
|
639
|
+
if (placement === 'side')
|
|
640
|
+
return buildTitleRichSideChildren(children, context, 'explainer:title-supports', { muted: true })
|
|
641
|
+
|
|
481
642
|
const title = readHeadingText(children, 'explainer:title-supports')
|
|
482
643
|
const list = children.find(isListNode)
|
|
483
|
-
const placement = options.placement ?? 'side'
|
|
484
644
|
const muted = options.muted ?? true
|
|
485
645
|
const supportSize = options.supportSize ?? '4'
|
|
486
646
|
|
|
@@ -490,12 +650,17 @@ function buildTitleSupportsChildren(
|
|
|
490
650
|
if (list.type !== 'ul')
|
|
491
651
|
failMarkdownContract('explainer:title-supports', 'ожидает ненумерованный markdown list (`-` или `*`).')
|
|
492
652
|
|
|
493
|
-
const
|
|
494
|
-
if (!
|
|
653
|
+
const items = readListItemNodes(list, 'explainer:title-supports')
|
|
654
|
+
if (!items.length || !items.every(hasListItemContent))
|
|
495
655
|
failMarkdownContract('explainer:title-supports', 'ожидает list items с текстом.')
|
|
496
656
|
|
|
497
|
-
if (
|
|
498
|
-
failMarkdownContract(
|
|
657
|
+
if (items.length !== 2) {
|
|
658
|
+
failMarkdownContract(
|
|
659
|
+
'explainer:title-supports',
|
|
660
|
+
'ожидает ровно два list items для нижнего 50/50 layout.',
|
|
661
|
+
'Нужны ровно 2 пункта `-` верхнего уровня под заголовком. Сожмите список или смените variant на title-supports / title-body.',
|
|
662
|
+
)
|
|
663
|
+
}
|
|
499
664
|
|
|
500
665
|
ensureOnlySupportedChildren('explainer:title-supports', children, node =>
|
|
501
666
|
node === title.node
|
|
@@ -503,57 +668,30 @@ function buildTitleSupportsChildren(
|
|
|
503
668
|
|| isBlankTextNode(node),
|
|
504
669
|
)
|
|
505
670
|
|
|
506
|
-
|
|
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
|
-
}
|
|
671
|
+
const bodyClass = muted ? 'Slide-TitleBody Slide-TitleBody_muted' : 'Slide-TitleBody'
|
|
518
672
|
|
|
519
673
|
return [
|
|
520
|
-
hSlot(context, 'primary', 'slide-explainer-title-supports-primary', {}, [
|
|
674
|
+
hSlot(context, 'primary', 'slide-explainer-title-supports-bottom-primary', {}, [
|
|
521
675
|
hText(context, 'h1', '6', title.text),
|
|
522
676
|
]),
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
677
|
+
...items.map((item, index) =>
|
|
678
|
+
hSlot(context, 'support', `slide-explainer-title-supports-bottom-support-${index + 1}`, {}, [
|
|
679
|
+
h(context.components.Text, {
|
|
680
|
+
as: 'div',
|
|
681
|
+
size: supportSize,
|
|
682
|
+
muted,
|
|
683
|
+
}, {
|
|
684
|
+
default: withCtx(() => [
|
|
685
|
+
h('div', { class: bodyClass }, readListItemContent(item)),
|
|
686
|
+
]),
|
|
687
|
+
}),
|
|
688
|
+
]),
|
|
689
|
+
),
|
|
526
690
|
]
|
|
527
691
|
}
|
|
528
692
|
|
|
529
|
-
function isRichTextBodyNode(node: VNode) {
|
|
530
|
-
return isParagraphNode(node) || isListNode(node) || isBlockquoteNode(node)
|
|
531
|
-
}
|
|
532
|
-
|
|
533
693
|
function buildTitleBodyChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
534
|
-
|
|
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
|
-
]
|
|
694
|
+
return buildTitleRichSideChildren(children, context, 'explainer:title-body')
|
|
557
695
|
}
|
|
558
696
|
|
|
559
697
|
function buildPointsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
@@ -726,6 +864,7 @@ function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext
|
|
|
726
864
|
const featuredIndex = metrics.findIndex(metric => metric.featured)
|
|
727
865
|
const featured = metrics[featuredIndex >= 0 ? featuredIndex : 0]
|
|
728
866
|
const supportMetrics = metrics.filter(metric => metric !== featured).slice(0, 2)
|
|
867
|
+
const supportMetricsFitGroup = `collection-${context.variant}-support-metrics`
|
|
729
868
|
|
|
730
869
|
if (supportMetrics.length < 2)
|
|
731
870
|
failMarkdownContract('collection:metrics', 'ожидает два вторичных metric item.')
|
|
@@ -758,7 +897,10 @@ function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext
|
|
|
758
897
|
]),
|
|
759
898
|
),
|
|
760
899
|
...supportMetrics.map((metric, index) =>
|
|
761
|
-
hSlot(context, 'support', `slide-collection-metrics-support-${index + 1}`, {
|
|
900
|
+
hSlot(context, 'support', `slide-collection-metrics-support-${index + 1}`, {
|
|
901
|
+
fitGroup: supportMetricsFitGroup,
|
|
902
|
+
gap: '2',
|
|
903
|
+
}, [
|
|
762
904
|
hText(context, 'div', '7', metric.value, { priority: 1 }),
|
|
763
905
|
hText(context, 'div', '2-6', metric.label, { priority: 2, muted: true, typography: 'compact' }),
|
|
764
906
|
]),
|
|
@@ -797,26 +939,45 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
797
939
|
)
|
|
798
940
|
|
|
799
941
|
const facts = readNestedListItems(list, 'collection:facts')
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
942
|
+
const expectedFactCount = FACTS_VARIANT_COUNTS[context.variant] ?? 3
|
|
943
|
+
|
|
944
|
+
if (facts.length !== expectedFactCount) {
|
|
945
|
+
failMarkdownContract(
|
|
946
|
+
'collection:facts',
|
|
947
|
+
`${context.variant} ожидает ровно ${expectedFactCount} факта в markdown-списке.`,
|
|
948
|
+
`Для ${context.variant} нужно ровно ${expectedFactCount} пункта верхнего уровня, у каждого — вложенная подпись.`,
|
|
949
|
+
)
|
|
950
|
+
}
|
|
803
951
|
|
|
804
952
|
facts.forEach((fact, index) => {
|
|
805
|
-
if (!fact.label)
|
|
806
|
-
failMarkdownContract(
|
|
953
|
+
if (!fact.label) {
|
|
954
|
+
failMarkdownContract(
|
|
955
|
+
'collection:facts',
|
|
956
|
+
`ожидает вложенный пункт с подписью у facts[${index}].`,
|
|
957
|
+
'Каждый факт: `- значение` и на следующей строке ` - подпись`. Не пишите `- роль: текст` одной строкой.',
|
|
958
|
+
)
|
|
959
|
+
}
|
|
807
960
|
})
|
|
808
961
|
|
|
809
|
-
const [featured, ...
|
|
810
|
-
const supportFacts = rest.slice(0, 2)
|
|
811
|
-
|
|
812
|
-
if (supportFacts.length < 2)
|
|
813
|
-
failMarkdownContract('collection:facts', 'ожидает минимум два дополнительных факта в markdown-списке.')
|
|
814
|
-
|
|
962
|
+
const [featured, ...supportFacts] = facts
|
|
815
963
|
const isFeatured = context.variant === 'facts-featured'
|
|
816
964
|
const isStacked = context.variant === 'facts-stacked'
|
|
965
|
+
const isDuo = context.variant === 'facts-duo'
|
|
966
|
+
const isQuartet = context.variant === 'facts-quartet'
|
|
967
|
+
const shouldCoordinateFactSizes = isStacked || isDuo || context.variant === 'facts-trio' || isQuartet
|
|
968
|
+
const factFitGroup = shouldCoordinateFactSizes ? `collection-${context.variant}-facts` : ''
|
|
969
|
+
const supportFactFitGroup = isFeatured ? `collection-${context.variant}-support-facts` : factFitGroup
|
|
817
970
|
const headingText = readInlineText(heading)
|
|
818
971
|
const descriptionText = description ? readInlineText(description) : ''
|
|
819
972
|
const footnoteText = footnote ? readInlineText(footnote) : ''
|
|
973
|
+
const rowFactSize: ThemeTextSizeInput = isQuartet ? '5-8' : isDuo ? '5-10' : '5-9'
|
|
974
|
+
const stackedFactSize: ThemeTextSizeInput = '5-9'
|
|
975
|
+
const stackedLabelSize: ThemeTextSizeInput = '2-4'
|
|
976
|
+
const rowLabelSize: ThemeTextSizeInput = '2-4'
|
|
977
|
+
const featuredFactSize: ThemeTextSizeInput = isFeatured ? '9-12' : isStacked ? stackedFactSize : rowFactSize
|
|
978
|
+
const supportFactSize: ThemeTextSizeInput = isFeatured ? '5-7' : isStacked ? stackedFactSize : rowFactSize
|
|
979
|
+
const featuredLabelSize: ThemeTextSizeInput = isFeatured ? '4-6' : isStacked ? stackedLabelSize : rowLabelSize
|
|
980
|
+
const supportLabelSize: ThemeTextSizeInput = isFeatured ? '2-4' : isStacked ? stackedLabelSize : rowLabelSize
|
|
820
981
|
|
|
821
982
|
const primaryChildren: VNode[] = [
|
|
822
983
|
hText(context, 'h2', isFeatured ? '5-7' : '6-8', headingText, { priority: 1 }),
|
|
@@ -825,12 +986,15 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
825
986
|
if (descriptionText)
|
|
826
987
|
primaryChildren.push(hText(context, 'p', isStacked ? '4-5' : '3-4', descriptionText, { priority: 2, muted: true }))
|
|
827
988
|
|
|
989
|
+
if (footnoteText && factFitGroup)
|
|
990
|
+
primaryChildren.push(hText(context, 'div', '2-3', footnoteText, { priority: 3, muted: true }))
|
|
991
|
+
|
|
828
992
|
const featuredChildren: VNode[] = [
|
|
829
|
-
hText(context, 'div',
|
|
830
|
-
hText(context, 'div',
|
|
993
|
+
hText(context, 'div', featuredFactSize, featured.value, { priority: 1 }),
|
|
994
|
+
hText(context, 'div', featuredLabelSize, featured.label, { priority: 2, muted: true }),
|
|
831
995
|
]
|
|
832
996
|
|
|
833
|
-
if (footnoteText)
|
|
997
|
+
if (footnoteText && !factFitGroup)
|
|
834
998
|
featuredChildren.push(hText(context, 'div', '2-3', footnoteText, { priority: 3, muted: true }))
|
|
835
999
|
|
|
836
1000
|
return [
|
|
@@ -838,11 +1002,15 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
838
1002
|
hSlot(context, 'support', `slide-collection-${context.variant}-support-1`, {
|
|
839
1003
|
gap: '2',
|
|
840
1004
|
centered: false,
|
|
1005
|
+
...(factFitGroup ? { fitGroup: factFitGroup } : {}),
|
|
841
1006
|
}, featuredChildren),
|
|
842
1007
|
...supportFacts.map((fact, index) =>
|
|
843
|
-
hSlot(context, 'support', `slide-collection-${context.variant}-support-${index + 2}`, {
|
|
844
|
-
|
|
845
|
-
|
|
1008
|
+
hSlot(context, 'support', `slide-collection-${context.variant}-support-${index + 2}`, {
|
|
1009
|
+
gap: '2',
|
|
1010
|
+
...(supportFactFitGroup ? { fitGroup: supportFactFitGroup } : {}),
|
|
1011
|
+
}, [
|
|
1012
|
+
hText(context, 'div', supportFactSize, fact.value, { priority: 1 }),
|
|
1013
|
+
hText(context, 'div', supportLabelSize, fact.label, { priority: 2, muted: true }),
|
|
846
1014
|
]),
|
|
847
1015
|
),
|
|
848
1016
|
]
|
|
@@ -851,7 +1019,9 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
851
1019
|
const LAYOUT_SHORTHANDS = {
|
|
852
1020
|
'collection:agenda': buildAgendaChildren,
|
|
853
1021
|
'collection:facts-stacked': buildFactsChildren,
|
|
1022
|
+
'collection:facts-duo': buildFactsChildren,
|
|
854
1023
|
'collection:facts-trio': buildFactsChildren,
|
|
1024
|
+
'collection:facts-quartet': buildFactsChildren,
|
|
855
1025
|
'collection:facts-featured': buildFactsChildren,
|
|
856
1026
|
'collection:points-trio': buildPointsChildren,
|
|
857
1027
|
'collection:timeline': buildTimelineChildren,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./markdown-to-vnodes.mjs')
|